انتقل إلى المحتوى

لم تُترجم هذه الصفحة بعد، لذا تُعرض بالإنجليزية.

Build a command-line tool

Write a command-line tool in TypeScript with Elide and no dependencies: argument parsing, files and stdin, color, prompts, exit codes and a sandbox.

اقرأ دون اتصالelide help cli-tools

في هذه الصفحة
الاستخدام مع الذكاء الاصطناعي
افتح في ChatGPTافتح في Claude

Elide runs a TypeScript file directly, so a command-line tool is one executable .ts file with no package.json, build step or dependencies. This page builds logscan, which counts HTTP status codes in access logs, and then adds a confirmation prompt, a sandbox and a time limit.

The tool

ts
#!/usr/bin/env elide
// logscan.ts
import { createReadStream, existsSync } from "node:fs";
import { createInterface } from "node:readline";
import { parseArgs, styleText } from "node:util";

const usage = `Usage: logscan [--json] [--min STATUS] [FILE...]

Counts HTTP status codes in access logs. Reads stdin when no FILE is given.`;

let args;
try {
  args = parseArgs({
    allowPositionals: true,
    options: {
      json: { type: "boolean" },
      min: { type: "string", default: "100" },
      help: { type: "boolean", short: "h" },
    },
  });
} catch (err) {
  console.error(`logscan: ${(err as Error).message}\n\n${usage}`);
  process.exit(2);
}
const { values, positionals } = args;
if (values.help) {
  console.log(usage);
  process.exit(0);
}
for (const file of positionals) {
  if (!existsSync(file)) {
    console.error(`logscan: no such file: ${file}`);
    process.exit(2);
  }
}

const min = Number(values.min);
const counts = new Map<number, number>();
const inputs = positionals.length
  ? positionals.map((file) => createReadStream(file))
  : [process.stdin];
for (const input of inputs) {
  for await (const line of createInterface({ input, crlfDelay: Infinity })) {
    const status = Number(line.trim().split(/\s+/).at(-1));
    if (status >= min) counts.set(status, (counts.get(status) ?? 0) + 1);
  }
}

const sorted = [...counts].sort(([a], [b]) => a - b);
if (values.json) {
  console.log(JSON.stringify(Object.fromEntries(sorted)));
} else {
  const color = process.stdout.isTTY && !process.env.NO_COLOR;
  for (const [status, count] of sorted) {
    const tint = status >= 500 ? "red" : status >= 400 ? "yellow" : "green";
    console.log(`${color ? styleText(tint, String(status)) : status}  ${count}`);
  }
}
process.exitCode = sorted.some(([status]) => status >= 500) ? 1 : 0;
utf-8 · ts · Lines: 59

Make it executable and give it a log:

Terminal
bash
chmod +x logscan.ts
printf 'GET /a 200\nGET /b 404\nPOST /a 200\nGET /c 500\nGET /d 404\n' > access.log
./logscan.ts access.log
text
200  2
404  2
500  1
utf-8 · text · Lines: 3

The exit status is 1 because the log has a 500. Scripts and CI steps can branch on it.

Arguments

parseArgs from node:util reads process.argv and returns typed values and positionals. An unknown option throws an error with code ERR_PARSE_ARGS_UNKNOWN_OPTION; the tool prints it with the usage and exits 2, the usual status for bad usage:

Terminal
bash
./logscan.ts --bogus
text
logscan: Unknown option '--bogus'

Usage: logscan [--json] [--min STATUS] [FILE...]

Counts HTTP status codes in access logs. Reads stdin when no FILE is given.
utf-8 · text · Lines: 5

Machine-readable output keeps the tool composable:

Terminal
bash
./logscan.ts --json --min 400 access.log
text
{"404":2,"500":1}
utf-8 · text · Lines: 1

Input

With no file arguments the tool reads stdin, so it works in a pipeline:

Terminal
bash
grep -v ' 500$' access.log | ./logscan.ts --min 400
text
404  2
utf-8 · text · Lines: 1

createInterface from node:readline turns any readable stream into lines. To read all of stdin at once, concatenate for await (const chunk of process.stdin) in a module.

Output and color

Results go to stdout and errors to stderr, so ./logscan.ts access.log > counts.txt still shows errors. Color only when stdout is a terminal and NO_COLOR is unset: logscan checks both before calling styleText, so redirected output and logs stay plain.

Ask before acting

Ask before changing anything, and never block a script waiting for an answer. This helper asks only on a terminal; anywhere else it answers no, and --yes skips the question:

ts
// clean.ts
import { readdirSync, rmSync } from "node:fs";
import { createInterface } from "node:readline/promises";
import { parseArgs } from "node:util";

async function confirm(question: string): Promise<boolean> {
  if (!process.stdin.isTTY) return false;
  const rl = createInterface({ input: process.stdin, output: process.stdout });
  try {
    return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
  } finally {
    rl.close();
  }
}

const { values } = parseArgs({ options: { yes: { type: "boolean", short: "y" } } });
const logs = readdirSync(".").filter((name) => name.endsWith(".log"));
if (!values.yes && !(await confirm(`Delete ${logs.length} log files?`))) {
  console.error("Nothing deleted; pass --yes to skip the question.");
  process.exit(1);
}
for (const name of logs) rmSync(name);
console.log(`Deleted ${logs.length} files.`);
utf-8 · ts · Lines: 23

In a terminal with two .log files:

text
Delete 2 log files? [y/N] y
Deleted 2 files.
utf-8 · text · Lines: 2

Elide flags in the shebang

env -S passes flags to Elide from the first line. Limit what the tool can read with sandbox flags, so a bug or a hostile log cannot reach the rest of the machine:

ts
#!/usr/bin/env -S elide --sandbox --allow-read=.
utf-8 · ts · Lines: 1

With that line, logscan reads files under the current directory, and ./logscan.ts /etc/hostname reports logscan: no such file: /etc/hostname because the read is denied.

Other flags that suit tools:

FlagEffect
--no-envSkip .env files in the working directory; see Environment and properties.
--timeout 30sStop the run after 30 seconds with exit status 124.
--max-mem 256mCap the heap.

Exit statuses Elide itself uses are listed in Exit codes.

Install it

Elide picks the language from the file name, so the tool keeps its .ts extension. To run it as plain logscan, put a two-line wrapper on your PATH; it runs the file through its shebang, flags included:

Terminal
bash
printf '#!/bin/sh\nexec "%s/logscan.ts" "$@"\n' "$PWD" > ~/.local/bin/logscan
chmod +x ~/.local/bin/logscan
logscan --help

Python tools work the same way with #!/usr/bin/env elide and argparse; see Python.

هل كانت هذه الصفحة مفيدة؟