add kv sh
This commit is contained in:
parent
b8230f55b3
commit
0a982b2458
7 changed files with 279 additions and 2 deletions
188
src/kvsh.ts
Executable file
188
src/kvsh.ts
Executable 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();
|
|
@ -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>;
|
Loading…
Add table
Add a link
Reference in a new issue