Skip to content

Commit

Permalink
feat: path API (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret authored Feb 12, 2023
1 parent 77c2e70 commit 230ecb5
Show file tree
Hide file tree
Showing 11 changed files with 1,554 additions and 179 deletions.
51 changes: 47 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ Cross platform shell tools for Deno inspired by [zx](https://github.com/google/z

## Differences with zx

1. Minimal globals or global configuration.
- Only a default instance of `$`, but it's not mandatory to use this.
1. No custom CLI.
1. Cross platform shell.
- Makes more code work on Windows.
- Allows exporting the shell's environment to the current process.
- Uses [deno_task_shell](https://github.com/denoland/deno_task_shell)'s parser.
1. Minimal globals or global configuration.
- Only a default instance of `$`, but it's not mandatory to use this.
1. No custom CLI.
1. Good for application code in addition to use as a shell script replacement.
1. Named after my cat.

Expand Down Expand Up @@ -470,6 +470,49 @@ pb.with(() => {
});
```

## Path API

The path API offers an immutable `PathReference` class, which is a similar concept to Rust's `PathBuf` struct.

To create a `PathReference`, do the following:

```ts
// create a `PathReference`
let srcDir = $.path("src");
// get information about the path
srcDir.isDir(); // false
// do actions on it
srcDir.mkdir();
srcDir.isDir(); // true

srcDir.isRelative(); // true
srcDir = srcDir.resolve(); // resolve the path to be absolute
srcDir.isRelative(); // false
srcDir.isAbsolute(); // true

// join to get other paths and do actions on them
const textFile = srcDir.join("file.txt");
textFile.writeTextSync("some text");
console.log(textFile.textSync()); // "some text"

const jsonFile = srcDir.join("subDir", "file.json");
jsonFile.parentOrThrow().mkdir();
jsonFile.writeJsonSync({
someValue: 5,
});
console.log(jsonFile.jsonSync().someValue); // 5
```

It also works to provide these paths to commands:

```ts
const srcDir = $.path("src").resolve();

await $`echo ${srcDir}`;
```

There are a lot of helper methods here, so check the documentation for more details.

## Helper functions

Changing the current working directory of the current process:
Expand Down Expand Up @@ -541,7 +584,7 @@ This line will appear without any indentation.
Empty lines (like the one above) will not affect the common indentation.
```

Re-export of deno_std's path:
Re-export of deno_std's path (though you might want to just use the path API described above):

```ts
$.path.basename("./deno/std/path/mod.ts"); // mod.ts
Expand Down
194 changes: 83 additions & 111 deletions mod.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { readAll } from "./src/deps.ts";
import $, { build$, CommandBuilder, CommandContext, CommandHandler } from "./mod.ts";
import { safeLstat } from "./src/common.ts";
import { assert, assertEquals, assertRejects, assertStringIncludes, assertThrows } from "./src/deps.test.ts";
import {
assert,
assertEquals,
assertRejects,
assertStringIncludes,
assertThrows,
withTempDir,
} from "./src/deps.test.ts";
import { Buffer, colors, path, readerFromStreamReader } from "./src/deps.ts";

Deno.test("should get stdout when piped", async () => {
Expand Down Expand Up @@ -812,7 +818,7 @@ Deno.test("streaming api errors while streaming", async () => {
}

{
const child = $`echo 1 && echo 2 && sleep 0.1 && exit 1`.stdout("piped").spawn();
const child = $`echo 1 && echo 2 && sleep 0.5 && exit 1`.stdout("piped").spawn();
const stdout = child.stdout();

const result = await $`deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable);'`
Expand Down Expand Up @@ -887,8 +893,7 @@ Deno.test("shebang support", async (t) => {
};

step("with -S", async () => {
await Deno.writeTextFile(
$.path.join(dir, "file.ts"),
dir.join("file.ts").writeTextSync(
[
"#!/usr/bin/env -S deno run",
"console.log(5);",
Expand All @@ -901,8 +906,7 @@ Deno.test("shebang support", async (t) => {
});

step("without -S and invalid", async () => {
await Deno.writeTextFile(
$.path.join(dir, "file2.ts"),
dir.join("file2.ts").writeTextSync(
[
"#!/usr/bin/env deno run",
"console.log(5);",
Expand All @@ -920,15 +924,13 @@ Deno.test("shebang support", async (t) => {
});

step("without -S, but valid", async () => {
await Deno.writeTextFile(
$.path.join(dir, "echo_stdin.ts"),
dir.join("echo_stdin.ts").writeTextSync(
[
"#!/usr/bin/env -S deno run --unstable --allow-run",
"await new Deno.Command('deno', { args: ['run', ...Deno.args] }).spawn();",
].join("\n"),
);
await Deno.writeTextFile(
$.path.join(dir, "file3.ts"),
dir.join("file3.ts").writeTextSync(
[
"#!/usr/bin/env ./echo_stdin.ts",
"console.log('Hello')",
Expand Down Expand Up @@ -1078,22 +1080,22 @@ Deno.test("environment should be evaluated at command execution", async () => {

Deno.test("test remove", async () => {
await withTempDir(async (dir) => {
const emptyDir = dir + "/hello";
const someFile = dir + "/a.txt";
const notExists = dir + "/notexists";
const emptyDir = dir.join("hello");
const someFile = dir.join("a.txt");
const notExists = dir.join("notexists");

Deno.mkdirSync(emptyDir);
Deno.writeTextFileSync(someFile, "");
emptyDir.mkdirSync();
someFile.writeTextSync("");

// Remove empty directory or file
await $`rm ${emptyDir}`;
await $`rm ${someFile}`;
assertEquals($.existsSync(dir + "/hello"), false);
assertEquals($.existsSync(dir + "/a.txt"), false);
assertEquals(emptyDir.existsSync(), false);
assertEquals(someFile.existsSync(), false);

// Remove a non-empty directory
const nonEmptyDir = dir + "/a";
Deno.mkdirSync(nonEmptyDir + "/b", { recursive: true });
const nonEmptyDir = dir.join("a");
nonEmptyDir.join("b").mkdirSync({ recursive: true });
{
const error = await $`rm ${nonEmptyDir}`.noThrow().stderr("piped").spawn()
.then((r) => r.stderr);
Expand All @@ -1104,7 +1106,7 @@ Deno.test("test remove", async () => {
}
{
await $`rm -r ${nonEmptyDir}`;
assertEquals($.existsSync(nonEmptyDir), false);
assertEquals(nonEmptyDir.existsSync(), false);
}

// Remove a directory that does not exist
Expand All @@ -1126,19 +1128,6 @@ Deno.test("test remove", async () => {
});
});

async function withTempDir(action: (path: string) => Promise<void>) {
const dirPath = Deno.makeTempDirSync();
try {
await action(dirPath);
} finally {
try {
await Deno.remove(dirPath, { recursive: true });
} catch {
// ignore
}
}
}

Deno.test("test mkdir", async () => {
await withTempDir(async (dir) => {
await $`mkdir ${dir}/a`;
Expand Down Expand Up @@ -1171,30 +1160,30 @@ Deno.test("test mkdir", async () => {

Deno.test("copy test", async () => {
await withTempDir(async (dir) => {
const file1 = path.join(dir, "file1.txt");
const file2 = path.join(dir, "file2.txt");
Deno.writeTextFileSync(file1, "test");
const file1 = dir.join("file1.txt");
const file2 = dir.join("file2.txt");
file1.writeTextSync("test");
await $`cp ${file1} ${file2}`;

assert($.existsSync(file1));
assert($.existsSync(file2));
assert(file1.existsSync());
assert(file2.existsSync());

const destDir = path.join(dir, "dest");
Deno.mkdirSync(destDir);
const destDir = dir.join("dest");
destDir.mkdirSync();
await $`cp ${file1} ${file2} ${destDir}`;

assert($.existsSync(file1));
assert($.existsSync(file2));
assert($.existsSync(path.join(destDir, "file1.txt")));
assert($.existsSync(path.join(destDir, "file2.txt")));
assert(file1.existsSync());
assert(file2.existsSync());
assert(destDir.join("file1.txt").existsSync());
assert(destDir.join("file2.txt").existsSync());

const newFile = path.join(dir, "new.txt");
Deno.writeTextFileSync(newFile, "test");
const newFile = dir.join("new.txt");
newFile.writeTextSync("test");
await $`cp ${newFile} ${destDir}`;

assert(await isDir(destDir));
assert($.existsSync(newFile));
assert($.existsSync(path.join(destDir, "new.txt")));
assert(destDir.isDir());
assert(newFile.existsSync());
assert(destDir.join("new.txt").existsSync());

assertEquals(
await getStdErr($`cp ${file1} ${file2} non-existent`),
Expand All @@ -1205,18 +1194,18 @@ Deno.test("copy test", async () => {
assertStringIncludes(await getStdErr($`cp ${file1} ""`), "cp: missing destination file operand after");

// recursive test
Deno.mkdirSync(path.join(destDir, "sub_dir"));
Deno.writeTextFileSync(path.join(destDir, "sub_dir", "sub.txt"), "test");
const destDir2 = path.join(dir, "dest2");
destDir.join("sub_dir").mkdirSync();
destDir.join("sub_dir", "sub.txt").writeTextSync("test");
const destDir2 = dir.join("dest2");

assertEquals(await getStdErr($`cp ${destDir} ${destDir2}`), "cp: source was a directory; maybe specify -r\n");
assert(!$.existsSync(destDir2));
assert(!destDir2.existsSync());

await $`cp -r ${destDir} ${destDir2}`;
assert($.existsSync(destDir2));
assert($.existsSync(path.join(destDir2, "file1.txt")));
assert($.existsSync(path.join(destDir2, "file2.txt")));
assert($.existsSync(path.join(destDir2, "sub_dir", "sub.txt")));
assert(destDir2.existsSync());
assert(destDir2.join("file1.txt").existsSync());
assert(destDir2.join("file2.txt").existsSync());
assert(destDir2.join("sub_dir", "sub.txt").existsSync());

// copy again
await $`cp -r ${destDir} ${destDir2}`;
Expand All @@ -1227,46 +1216,40 @@ Deno.test("copy test", async () => {
});

Deno.test("cp test2", async () => {
await withTempDir(async (dir) => {
const originalDir = Deno.cwd();
try {
Deno.chdir(dir);
await $`mkdir -p a/d1`;
await $`mkdir -p a/d2`;
Deno.createSync("a/d1/f").close();
await $`cp a/d1/f a/d2`;
assert($.existsSync("a/d2/f"));
} finally {
Deno.chdir(originalDir);
}
await withTempDir(async () => {
await $`mkdir -p a/d1`;
await $`mkdir -p a/d2`;
Deno.createSync("a/d1/f").close();
await $`cp a/d1/f a/d2`;
assert($.existsSync("a/d2/f"));
});
});

Deno.test("move test", async () => {
await withTempDir(async (dir) => {
const file1 = path.join(dir, "file1.txt");
const file2 = path.join(dir, "file2.txt");
Deno.writeTextFileSync(file1, "test");
const file1 = dir.join("file1.txt");
const file2 = dir.join("file2.txt");
file1.writeTextSync("test");

await $`mv ${file1} ${file2}`;
assert(!$.existsSync(file1));
assert($.existsSync(file2));
assert(!file1.existsSync());
assert(file2.existsSync());

const destDir = path.join(dir, "dest");
Deno.writeTextFileSync(file1, "test"); // recreate
Deno.mkdirSync(destDir);
const destDir = dir.join("dest");
file1.writeTextSync("test"); // recreate
destDir.mkdirSync();
await $`mv ${file1} ${file2} ${destDir}`;
assert(!$.existsSync(file1));
assert(!$.existsSync(file2));
assert($.existsSync(path.join(destDir, "file1.txt")));
assert($.existsSync(path.join(destDir, "file2.txt")));
assert(!file1.existsSync());
assert(!file2.existsSync());
assert(destDir.join("file1.txt").existsSync());
assert(destDir.join("file2.txt").existsSync());

const newFile = path.join(dir, "new.txt");
Deno.writeTextFileSync(newFile, "test");
const newFile = dir.join("new.txt");
newFile.writeTextSync("test");
await $`mv ${newFile} ${destDir}`;
assert(await isDir(destDir));
assert(!$.existsSync(newFile));
assert($.existsSync(path.join(destDir, "new.txt")));
assert(destDir.isDir());
assert(!newFile.existsSync());
assert(destDir.join("new.txt").existsSync());

assertEquals(
await getStdErr($`mv ${file1} ${file2} non-existent`),
Expand Down Expand Up @@ -1307,11 +1290,6 @@ async function getStdErr(cmd: CommandBuilder) {
return await cmd.noThrow().stderr("piped").then((r) => r.stderr);
}

async function isDir(path: string) {
const info = await safeLstat(path);
return info?.isDirectory ? true : false;
}

Deno.test("$.commandExists", async () => {
assertEquals(await $.commandExists("some-fake-command"), false);
assertEquals(await $.commandExists("deno"), true);
Expand Down Expand Up @@ -1361,24 +1339,18 @@ Empty lines (like the one above) will not affect the common indentation.`.trim()
});

Deno.test("touch test", async () => {
await withTempDir(async (dir) => {
const originalDir = Deno.cwd();
try {
Deno.chdir(dir);
await $`touch a`;
assert($.existsSync("a"));
await $`touch a`;
assert($.existsSync("a"));

await $`touch b c`;
assert($.existsSync("b"));
assert($.existsSync("c"));

assertEquals(await getStdErr($`touch`), "touch: missing file operand\n");

assertEquals(await getStdErr($`touch --test hello`), "touch: unsupported flag: --test\n");
} finally {
Deno.chdir(originalDir);
}
await withTempDir(async () => {
await $`touch a`;
assert($.existsSync("a"));
await $`touch a`;
assert($.existsSync("a"));

await $`touch b c`;
assert($.existsSync("b"));
assert($.existsSync("c"));

assertEquals(await getStdErr($`touch`), "touch: missing file operand\n");

assertEquals(await getStdErr($`touch --test hello`), "touch: unsupported flag: --test\n");
});
});
Loading

0 comments on commit 230ecb5

Please sign in to comment.