add kv sh

This commit is contained in:
JOLIMAITRE Matthieu 2024-05-30 00:09:31 +02:00
parent b8230f55b3
commit 0a982b2458
7 changed files with 279 additions and 2 deletions

188
src/kvsh.ts Executable file
View file

@ -0,0 +1,188 @@
#!/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("<path:string>").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 <path>");
const resolved = this.resolve(arg);
this.pwd = resolved;
}
private async run_cat(args: string[]) {
if (args.length < 1) return new Failure("Usage: cat <entry> [...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<string>();
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();

View file

@ -1,5 +1,5 @@
import { Collection, Entry } from "./entry.ts";
import { Store } from "./store.ts";
import { Collection, Entry } from "../lib/entry.ts";
import { Store } from "../lib/store.ts";
export type Table<Self extends Table<Self>> = {
[Name: string]: Structure<Self>;