Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mkdir command #34

Merged
merged 4 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion mod.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -749,3 +749,33 @@ async function withTempDir(action: (path: string) => Promise<void>) {
}
}
}

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"));
});
});
2 changes: 2 additions & 0 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -45,6 +46,7 @@ const builtInCommands = {
sleep: sleepCommand,
test: testCommand,
rm: rmCommand,
mkdir: mkdirCommand,
};

/**
Expand Down
11 changes: 11 additions & 0 deletions src/commands/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
}
49 changes: 49 additions & 0 deletions src/commands/mkdir.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
64 changes: 64 additions & 0 deletions src/commands/mkdir.ts
Original file line number Diff line number Diff line change
@@ -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<ExecuteResult> {
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;
}
23 changes: 5 additions & 18 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -9,23 +9,23 @@ export async function testCommand(context: CommandContext): Promise<ExecuteResul
let result: Promise<boolean>;
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":
result = fs.exists(testPath);
break;

case "-s":
result = stat(testPath, (info) => 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:
Expand All @@ -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;
}
}
}
16 changes: 16 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,19 @@ export class TreeBox<T> {
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;
}
}
}