diff --git a/mod.test.ts b/mod.test.ts index 4ffbead..7d1de9f 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -1,5 +1,5 @@ import $, { build$, CommandBuilder, CommandContext, CommandHandler } from "./mod.ts"; -import { assertEquals, assertRejects, assertThrows } from "./src/deps.test.ts"; +import { assert, assertEquals, assertRejects, assertThrows } from "./src/deps.test.ts"; import { Buffer, colors, path } from "./src/deps.ts"; Deno.test("should get stdout when piped", async () => { @@ -749,3 +749,33 @@ async function withTempDir(action: (path: string) => Promise) { } } } + +Deno.test("test mkdir", async () => { + await withTempDir(async (dir) => { + await $`mkdir ${dir}/a`; + await $.exists(dir + "/a"); + + { + const error = await $`mkdir ${dir}/a`.noThrow().stderr("piped").spawn() + .then( + (r) => r.stderr, + ); + const expecteError = "mkdir: cannot create directory"; + assertEquals(error.slice(0, expecteError.length), expecteError); + } + + { + const error = await $`mkdir ${dir}/b/c`.noThrow().stderr("piped").spawn() + .then( + (r) => r.stderr, + ); + const expectedError = Deno.build.os === "windows" + ? "mkdir: The system cannot find the path specified." + : "mkdir: No such file or directory"; + assertEquals(error.slice(0, expectedError.length), expectedError); + } + + await $`mkdir -p ${dir}/b/c`; + assert(await $.exists(dir + "/b/c")); + }); +}); diff --git a/src/command.ts b/src/command.ts index 29eb963..b044d78 100644 --- a/src/command.ts +++ b/src/command.ts @@ -3,6 +3,7 @@ import { cdCommand } from "./commands/cd.ts"; import { echoCommand } from "./commands/echo.ts"; import { exitCommand } from "./commands/exit.ts"; import { exportCommand } from "./commands/export.ts"; +import { mkdirCommand } from "./commands/mkdir.ts"; import { rmCommand } from "./commands/rm.ts"; import { sleepCommand } from "./commands/sleep.ts"; import { testCommand } from "./commands/test.ts"; @@ -45,6 +46,7 @@ const builtInCommands = { sleep: sleepCommand, test: testCommand, rm: rmCommand, + mkdir: mkdirCommand, }; /** diff --git a/src/commands/args.ts b/src/commands/args.ts index 6a83c9e..0daa67d 100644 --- a/src/commands/args.ts +++ b/src/commands/args.ts @@ -30,3 +30,14 @@ export function parse_arg_kinds(flags: string[]): ArgKind[] { } return result; } + +export function bailUnsupported(arg: ArgKind): never { + switch (arg.kind) { + case "Arg": + throw Error(`unsupported argument: ${arg.arg}`); + case "ShortFlag": + throw Error(`unsupported flag: -${arg.arg}`); + case "LongFlag": + throw Error(`unsupported flag: --${arg.arg}`); + } +} diff --git a/src/commands/mkdir.test.ts b/src/commands/mkdir.test.ts new file mode 100644 index 0000000..b24f4c3 --- /dev/null +++ b/src/commands/mkdir.test.ts @@ -0,0 +1,49 @@ +import { assertEquals, assertThrows } from "../deps.test.ts"; +import { parseArgs } from "./mkdir.ts"; + +Deno.test("test mkdir parse args", () => { + assertEquals( + parseArgs([ + "--parents", + "a", + "b", + ]), + { + parents: true, + paths: ["a", "b"], + }, + ); + assertEquals( + parseArgs(["-p", "a", "b"]), + { + parents: true, + paths: ["a", "b"], + }, + ); + assertThrows( + () => parseArgs(["--parents"]), + Error, + "missing operand", + ); + assertThrows( + () => + parseArgs([ + "--parents", + "-p", + "-u", + "a", + ]), + Error, + "unsupported flag: -u", + ); + assertThrows( + () => + parseArgs([ + "--parents", + "--random-flag", + "a", + ]), + Error, + "unsupported flag: --random-flag", + ); +}); diff --git a/src/commands/mkdir.ts b/src/commands/mkdir.ts new file mode 100644 index 0000000..be991fb --- /dev/null +++ b/src/commands/mkdir.ts @@ -0,0 +1,64 @@ +import { CommandContext } from "../command_handler.ts"; +import { resolvePath } from "../common.ts"; +import { ExecuteResult, resultFromCode } from "../result.ts"; +import { lstat } from "../common.ts"; +import { bailUnsupported, parse_arg_kinds } from "./args.ts"; + +export async function mkdirCommand( + context: CommandContext, +): Promise { + try { + await executeMkdir(context.cwd, context.args); + return resultFromCode(0); + } catch (err) { + context.stderr.writeLine(`mkdir: ${err?.message ?? err}`); + return resultFromCode(1); + } +} + +interface MkdirFlags { + parents: boolean; + paths: string[]; +} + +async function executeMkdir(cwd: string, args: string[]) { + const flags = parseArgs(args); + for (const specifiedPath of flags.paths) { + const path = resolvePath(cwd, specifiedPath); + if ( + await lstat(path, (info) => info.isFile) || + (!flags.parents && + await lstat(path, (info) => info.isDirectory)) + ) { + throw Error(`cannot create directory '${specifiedPath}': File exists`); + } + if (flags.parents) { + await Deno.mkdir(path, { recursive: true }); + } else { + await Deno.mkdir(path); + } + } +} + +export function parseArgs(args: string[]) { + const result: MkdirFlags = { + parents: false, + paths: [], + }; + + for (const arg of parse_arg_kinds(args)) { + if ( + (arg.arg === "parents" && arg.kind === "LongFlag") || + (arg.arg === "p" && arg.kind == "ShortFlag") + ) { + result.parents = true; + } else { + if (arg.kind !== "Arg") bailUnsupported(arg); + result.paths.push(arg.arg.trim()); // NOTE: rust version doesn't trim + } + } + if (result.paths.length === 0) { + throw Error("missing operand"); + } + return result; +} diff --git a/src/commands/test.ts b/src/commands/test.ts index 0c55a5a..0d61287 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,5 +1,5 @@ import { CommandContext } from "../command_handler.ts"; -import { resolvePath } from "../common.ts"; +import { lstat, resolvePath } from "../common.ts"; import { fs } from "../deps.ts"; import { ExecuteResult, resultFromCode } from "../result.ts"; @@ -9,11 +9,11 @@ export async function testCommand(context: CommandContext): Promise; switch (testFlag) { case "-f": - result = stat(testPath, (info) => info.isFile); + result = lstat(testPath, (info) => info.isFile); break; case "-d": - result = stat(testPath, (info) => info.isDirectory); + result = lstat(testPath, (info) => info.isDirectory); break; case "-e": @@ -21,11 +21,11 @@ export async function testCommand(context: CommandContext): Promise info.size > 0); + result = lstat(testPath, (info) => info.size > 0); break; case "-L": - result = stat(testPath, (info) => info.isSymlink); + result = lstat(testPath, (info) => info.isSymlink); break; default: @@ -50,16 +50,3 @@ function parseArgs(cwd: string, args: string[]) { return [args[0], resolvePath(cwd, args[1])]; } - -async function stat(path: string, test: (info: Deno.FileInfo) => boolean) { - try { - const info = await Deno.lstat(path); - return test(info); - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } else { - throw err; - } - } -} diff --git a/src/common.ts b/src/common.ts index a14be6c..885e194 100644 --- a/src/common.ts +++ b/src/common.ts @@ -92,3 +92,19 @@ export class TreeBox { return new TreeBox(this); } } + +export async function lstat( + path: string, + test: (info: Deno.FileInfo) => boolean, +) { + try { + const info = await Deno.lstat(path); + return test(info); + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } else { + throw err; + } + } +}