#!/usr/bin/env -S deno run -A --unstable-kv import { TextLineStream } from "https://deno.land/std@0.224.0/streams/text_line_stream.ts"; import { writeAll } from "https://deno.land/std@0.224.0/io/write_all.ts"; import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts"; async function main() { const parsed = await new Command().name("kvsh").arguments("").parse(); const [path] = parsed.args; const kv = await Deno.openKv(path); const context = new Context(kv); const line_stream = Deno.stdin.readable .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); context.init(); for await (const line of line_stream) context.run(line); } class Context { kv; pwd; public constructor(kv: Deno.Kv) { this.kv = kv; this.pwd = [] as string[]; } public async init() { console.log(` kvsh shell use 'help' for more info `); await this.print_prefix(); } public async run(line: string) { const result = await this.try_run(line); if (result instanceof Failure) console.log("Failure :", result.message); await this.print_prefix(); } private async print_prefix() { const prefix = ` /${this.pwd.join("/")} > `; await writeAll(Deno.stdout, new TextEncoder().encode(prefix)); } private async try_run(line: string) { const parsed = parse_line(line); if (parsed instanceof Failure) return parsed; if (parsed.cmd === "help") console.log("Available commands:\nhelp cd ls cat find clear"); if (parsed.cmd === "clear") console.clear(); if (parsed.cmd === "ls") return await this.run_ls(parsed.args); if (parsed.cmd === "cd") return this.run_cd(parsed.arg); if (parsed.cmd === "cat") return await this.run_cat(parsed.args); if (parsed.cmd === "find") return await this.run_find(parsed.args); } private async run_ls(args: string[]) { if (args.length === 0) args.push("."); for (const arg of args) { if (args.length > 1) console.log(`${arg}:`); await ls(this.kv, this.resolve(arg)); } } private run_cd(arg: string) { if (arg === undefined) return new Failure("Usage: cd "); const resolved = this.resolve(arg); this.pwd = resolved; } private async run_cat(args: string[]) { if (args.length < 1) return new Failure("Usage: cat [...entries]"); for (const arg of args) { const resolved = this.resolve(arg); const entry = await this.kv.get(resolved); console.log(JSON.stringify(entry.value)); } } private async run_find(args: string[]) { if (args.length < 1) args.push("."); for (const arg of args) { const prefix = this.resolve(arg); for await (const entry of this.kv.list({ prefix })) { const parts = entry.key.map((part) => part.toString().replaceAll('"', '\\"').replaceAll(" ", "\\ ")); console.log(`/${parts.join("/")}`); } } } private resolve(path: string) { let base = this.pwd; if (path.startsWith("/")) base = [], path = path.slice(1); const acc = [] as string[]; for (const part of [...base, ...path.split("/")]) { if (part === ".") continue; if (part === "..") acc.pop(); else acc.push(part); } return acc; } } async function ls(kv: Deno.Kv, prefix: string[]) { const result_set = new Set(); for await (const entry of kv.list({ prefix })) { const name = entry.key[prefix.length].toString() .replaceAll('"', '\\"') .replaceAll(" ", "\\ "); const is_file = entry.key.length === prefix.length + 1; result_set.add(`${is_file ? "f" : "d"} ${name}`); } const results = Array.from(result_set.values()); const column_width = Math.max(...results.map((w) => w.length)) + 2; const column_count = 80 / column_width; const per_columns = (result_set.size / column_count) + 1; const columns = [] as string[][]; for (const index of range(0, column_count)) { columns.push(results.slice(index * per_columns, (index + 1) * per_columns)); } for (const index of range(0, per_columns)) { const items = columns.map((c) => c[index]).filter((item) => item != undefined); const line = items.map((item) => item.padEnd(column_width)).join(""); if (line === "") continue; console.log(line); } } function parse_line(line: string) { const parts = split_words(line); if (parts === null) return new Failure("Unclosed capture."); const [cmd, ...args] = parts, [arg] = args; if (cmd === "help") return { cmd } as const; if (cmd === "clear") return { cmd } as const; if (cmd === "cd") return { cmd, arg } as const; if (cmd === "ls") return { cmd, args } as const; if (cmd === "cat") return { cmd, args } as const; if (cmd === "find") return { cmd, args } as const; else return new Failure(`Unknown command '${cmd}'`); } function split_words(text: string) { const words = [] as string[]; let capturing = false; let escaping = false; let current = ""; const rotate_current = () => (words.push(current), current = ""); for (const character of text) { if (escaping) { current += character; escaping = false; } else if (character === "\\") escaping = true; else if (character === '"') capturing = !capturing; else if (character === " ") { if (capturing) current += character; else if (current.length > 0) rotate_current(); } else current += character; } if (capturing) return null; if (current.length > 0) rotate_current(); return words; } Deno.test("test_split_words", async () => { const { assertEquals } = await import("https://deno.land/std@0.224.0/assert/assert_equals.ts"); assertEquals(split_words('arbre "mort \\\\ "'), ["arbre", "mort \\ "]); }); class Failure { public readonly message; public constructor(message: string) { this.message = message; } } function* range(from: number, to: number) { while (from < to) yield from++; } if (import.meta.main) await main();