diff options
Diffstat (limited to 'std/fs')
68 files changed, 7874 insertions, 0 deletions
diff --git a/std/fs/README.md b/std/fs/README.md new file mode 100644 index 000000000..b774e061a --- /dev/null +++ b/std/fs/README.md @@ -0,0 +1,220 @@ +# fs + +fs module is made to provide helpers to manipulate the filesystem. + +## Usage + +All the following modules are exposed in `mod.ts` + +### emptyDir + +Ensures that a directory is empty. Deletes directory contents if the directory is not empty. +If the directory does not exist, it is created. +The directory itself is not deleted. + +```ts +import { emptyDir, emptyDirSync } from "https://deno.land/std/fs/mod.ts"; + +emptyDir("./foo"); // returns a promise +emptyDirSync("./foo"); // void +``` + +### ensureDir + +Ensures that the directory exists. +If the directory structure does not exist, it is created. Like mkdir -p. + +```ts +import { ensureDir, ensureDirSync } from "https://deno.land/std/fs/mod.ts"; + +ensureDir("./bar"); // returns a promise +ensureDirSync("./ensureDirSync"); // void +``` + +### ensureFile + +Ensures that the file exists. +If the file that is requested to be created is in directories +that do not exist, these directories are created. +If the file already exists, it is **NOT MODIFIED**. + +```ts +import { ensureFile, ensureFileSync } from "https://deno.land/std/fs/mod.ts"; + +ensureFile("./folder/targetFile.dat"); // returns promise +ensureFileSync("./folder/targetFile.dat"); // void +``` + +### ensureSymlink + +Ensures that the link exists. +If the directory structure does not exist, it is created. + +```ts +import { + ensureSymlink, + ensureSymlinkSync +} from "https://deno.land/std/fs/mod.ts"; + +ensureSymlink( + "./folder/targetFile.dat", + "./folder/targetFile.link.dat", + "file" +); // returns promise +ensureSymlinkSync( + "./folder/targetFile.dat", + "./folder/targetFile.link.dat", + "file" +); // void +``` + +### eol + +Detects and format the passed string for the targeted End Of Line character. + +```ts +import { format, detect, EOL } from "https://deno.land/std/fs/mod.ts"; + +const CRLFinput = "deno\r\nis not\r\nnode"; +const Mixedinput = "deno\nis not\r\nnode"; +const LFinput = "deno\nis not\nnode"; +const NoNLinput = "deno is not node"; + +detect(LFinput); // output EOL.LF +detect(CRLFinput); // output EOL.CRLF +detect(Mixedinput); // output EOL.CRLF +detect(NoNLinput); // output null + +format(CRLFinput, EOL.LF); // output "deno\nis not\nnode" +... +``` + +### exists + +Test whether or not the given path exists by checking with the file system + +```ts +import { exists, existsSync } from "https://deno.land/std/fs/mod.ts"; + +exists("./foo"); // returns a Promise<boolean> +existsSync("./foo"); // returns boolean +``` + +### globToRegExp + +Generate a regex based on glob pattern and options +This was meant to be using the the `fs.walk` function +but can be used anywhere else. + +```ts +import { globToRegExp } from "https://deno.land/std/fs/mod.ts"; + +globToRegExp("foo/**/*.json", { + flags: "g", + extended: true, + globstar: true +}); // returns the regex to find all .json files in the folder foo +``` + +### move + +Moves a file or directory. Overwrites it if option provided + +```ts +import { move, moveSync } from "https://deno.land/std/fs/mod.ts"; + +move("./foo", "./bar"); // returns a promise +moveSync("./foo", "./bar"); // void +moveSync("./foo", "./existingFolder", { overwrite: true }); +// Will overwrite existingFolder +``` + +### copy + +copy a file or directory. Overwrites it if option provided + +```ts +import { copy, copySync } from "https://deno.land/std/fs/mod.ts"; + +copy("./foo", "./bar"); // returns a promise +copySync("./foo", "./bar"); // void +copySync("./foo", "./existingFolder", { overwrite: true }); +// Will overwrite existingFolder +``` + +### readJson + +Reads a JSON file and then parses it into an object + +```ts +import { readJson, readJsonSync } from "https://deno.land/std/fs/mod.ts"; + +const f = await readJson("./foo.json"); +const foo = readJsonSync("./foo.json"); +``` + +### walk + +Iterate all files in a directory recursively. + +```ts +import { walk, walkSync } from "https://deno.land/std/fs/mod.ts"; + +for (const fileInfo of walkSync(".")) { + console.log(fileInfo.filename); +} + +// Async +async function printFilesNames() { + for await (const fileInfo of walk()) { + console.log(fileInfo.filename); + } +} + +printFilesNames().then(() => console.log("Done!")); +``` + +### writeJson + +Writes an object to a JSON file. + +**WriteJsonOptions** + +- replacer : An array of strings and numbers that acts as a approved list for selecting the object properties that will be stringified. +- space : Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + +```ts +import { writeJson, writeJsonSync } from "https://deno.land/std/fs/mod.ts"; + +writeJson("./target.dat", { foo: "bar" }, { spaces: 2 }); // returns a promise +writeJsonSync("./target.dat", { foo: "bar" }, { replacer: ["foo"] }); // void +``` + +### readFileStr + +Read file and output it as a string. + +**ReadOptions** + +- encoding : The encoding to read file. lowercased. + +```ts +import { readFileStr, readFileStrSync } from "https://deno.land/std/fs/mod.ts"; + +readFileStr("./target.dat", { encoding: "utf8" }); // returns a promise +readFileStrSync("./target.dat", { encoding: "utf8" }); // void +``` + +### writeFileStr + +Write the string to file. + +```ts +import { + writeFileStr, + writeFileStrSync +} from "https://deno.land/std/fs/mod.ts"; + +writeFileStr("./target.dat", "file content"); // returns a promise +writeFileStrSync("./target.dat", "file content"); // void +``` diff --git a/std/fs/copy.ts b/std/fs/copy.ts new file mode 100644 index 000000000..616fba975 --- /dev/null +++ b/std/fs/copy.ts @@ -0,0 +1,261 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import * as path from "./path/mod.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import { isSubdir, getFileInfoType } from "./utils.ts"; + +export interface CopyOptions { + /** + * overwrite existing file or directory. Default is `false` + */ + overwrite?: boolean; + /** + * When `true`, will set last modification and access times to the ones of the + * original source files. + * When `false`, timestamp behavior is OS-dependent. + * Default is `false`. + */ + preserveTimestamps?: boolean; +} + +async function ensureValidCopy( + src: string, + dest: string, + options: CopyOptions, + isCopyFolder = false +): Promise<Deno.FileInfo> { + const destStat: Deno.FileInfo | null = await Deno.lstat(dest).catch( + (): Promise<null> => Promise.resolve(null) + ); + + if (destStat) { + if (isCopyFolder && !destStat.isDirectory()) { + throw new Error( + `Cannot overwrite non-directory '${dest}' with directory '${src}'.` + ); + } + if (!options.overwrite) { + throw new Error(`'${dest}' already exists.`); + } + } + + return destStat!; +} + +function ensureValidCopySync( + src: string, + dest: string, + options: CopyOptions, + isCopyFolder = false +): Deno.FileInfo { + let destStat: Deno.FileInfo | null; + + try { + destStat = Deno.lstatSync(dest); + } catch { + // ignore error + } + + if (destStat!) { + if (isCopyFolder && !destStat!.isDirectory()) { + throw new Error( + `Cannot overwrite non-directory '${dest}' with directory '${src}'.` + ); + } + if (!options.overwrite) { + throw new Error(`'${dest}' already exists.`); + } + } + + return destStat!; +} + +/* copy file to dest */ +async function copyFile( + src: string, + dest: string, + options: CopyOptions +): Promise<void> { + await ensureValidCopy(src, dest, options); + await Deno.copyFile(src, dest); + if (options.preserveTimestamps) { + const statInfo = await Deno.stat(src); + await Deno.utime(dest, statInfo.accessed!, statInfo.modified!); + } +} +/* copy file to dest synchronously */ +function copyFileSync(src: string, dest: string, options: CopyOptions): void { + ensureValidCopySync(src, dest, options); + Deno.copyFileSync(src, dest); + if (options.preserveTimestamps) { + const statInfo = Deno.statSync(src); + Deno.utimeSync(dest, statInfo.accessed!, statInfo.modified!); + } +} + +/* copy symlink to dest */ +async function copySymLink( + src: string, + dest: string, + options: CopyOptions +): Promise<void> { + await ensureValidCopy(src, dest, options); + const originSrcFilePath = await Deno.readlink(src); + const type = getFileInfoType(await Deno.lstat(src)); + await Deno.symlink(originSrcFilePath, dest, type); + if (options.preserveTimestamps) { + const statInfo = await Deno.lstat(src); + await Deno.utime(dest, statInfo.accessed!, statInfo.modified!); + } +} + +/* copy symlink to dest synchronously */ +function copySymlinkSync( + src: string, + dest: string, + options: CopyOptions +): void { + ensureValidCopySync(src, dest, options); + const originSrcFilePath = Deno.readlinkSync(src); + const type = getFileInfoType(Deno.lstatSync(src)); + Deno.symlinkSync(originSrcFilePath, dest, type); + if (options.preserveTimestamps) { + const statInfo = Deno.lstatSync(src); + Deno.utimeSync(dest, statInfo.accessed!, statInfo.modified!); + } +} + +/* copy folder from src to dest. */ +async function copyDir( + src: string, + dest: string, + options: CopyOptions +): Promise<void> { + const destStat = await ensureValidCopy(src, dest, options, true); + + if (!destStat) { + await ensureDir(dest); + } + + if (options.preserveTimestamps) { + const srcStatInfo = await Deno.stat(src); + await Deno.utime(dest, srcStatInfo.accessed!, srcStatInfo.modified!); + } + + const files = await Deno.readDir(src); + + for (const file of files) { + const srcPath = path.join(src, file.name!); + const destPath = path.join(dest, path.basename(srcPath as string)); + if (file.isDirectory()) { + await copyDir(srcPath, destPath, options); + } else if (file.isFile()) { + await copyFile(srcPath, destPath, options); + } else if (file.isSymlink()) { + await copySymLink(srcPath, destPath, options); + } + } +} + +/* copy folder from src to dest synchronously */ +function copyDirSync(src: string, dest: string, options: CopyOptions): void { + const destStat: Deno.FileInfo = ensureValidCopySync(src, dest, options, true); + + if (!destStat) { + ensureDirSync(dest); + } + + if (options.preserveTimestamps) { + const srcStatInfo = Deno.statSync(src); + Deno.utimeSync(dest, srcStatInfo.accessed!, srcStatInfo.modified!); + } + + const files = Deno.readDirSync(src); + + for (const file of files) { + const srcPath = path.join(src, file.name!); + const destPath = path.join(dest, path.basename(srcPath as string)); + if (file.isDirectory()) { + copyDirSync(srcPath, destPath, options); + } else if (file.isFile()) { + copyFileSync(srcPath, destPath, options); + } else if (file.isSymlink()) { + copySymlinkSync(srcPath, destPath, options); + } + } +} + +/** + * Copy a file or directory. The directory can have contents. Like `cp -r`. + * @param src the file/directory path. + * Note that if `src` is a directory it will copy everything inside + * of this directory, not the entire directory itself + * @param dest the destination path. Note that if `src` is a file, `dest` cannot + * be a directory + * @param options + */ +export async function copy( + src: string, + dest: string, + options: CopyOptions = {} +): Promise<void> { + src = path.resolve(src); + dest = path.resolve(dest); + + if (src === dest) { + throw new Error("Source and destination cannot be the same."); + } + + const srcStat = await Deno.lstat(src); + + if (srcStat.isDirectory() && isSubdir(src, dest)) { + throw new Error( + `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.` + ); + } + + if (srcStat.isDirectory()) { + await copyDir(src, dest, options); + } else if (srcStat.isFile()) { + await copyFile(src, dest, options); + } else if (srcStat.isSymlink()) { + await copySymLink(src, dest, options); + } +} + +/** + * Copy a file or directory. The directory can have contents. Like `cp -r`. + * @param src the file/directory path. + * Note that if `src` is a directory it will copy everything inside + * of this directory, not the entire directory itself + * @param dest the destination path. Note that if `src` is a file, `dest` cannot + * be a directory + * @param options + */ +export function copySync( + src: string, + dest: string, + options: CopyOptions = {} +): void { + src = path.resolve(src); + dest = path.resolve(dest); + + if (src === dest) { + throw new Error("Source and destination cannot be the same."); + } + + const srcStat = Deno.lstatSync(src); + + if (srcStat.isDirectory() && isSubdir(src, dest)) { + throw new Error( + `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.` + ); + } + + if (srcStat.isDirectory()) { + copyDirSync(src, dest, options); + } else if (srcStat.isFile()) { + copyFileSync(src, dest, options); + } else if (srcStat.isSymlink()) { + copySymlinkSync(src, dest, options); + } +} diff --git a/std/fs/copy_test.ts b/std/fs/copy_test.ts new file mode 100644 index 000000000..347d2532c --- /dev/null +++ b/std/fs/copy_test.ts @@ -0,0 +1,553 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrows, + assertThrowsAsync, + assert +} from "../testing/asserts.ts"; +import { copy, copySync } from "./copy.ts"; +import { exists, existsSync } from "./exists.ts"; +import * as path from "./path/mod.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import { ensureFile, ensureFileSync } from "./ensure_file.ts"; +import { ensureSymlink, ensureSymlinkSync } from "./ensure_symlink.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +// TODO(axetroy): Add test for Windows once symlink is implemented for Windows. +const isWindows = Deno.build.os === "win"; + +async function testCopy( + name: string, + cb: (tempDir: string) => Promise<void> +): Promise<void> { + test({ + name, + async fn(): Promise<void> { + const tempDir = await Deno.makeTempDir({ + prefix: "deno_std_copy_async_test_" + }); + await cb(tempDir); + await Deno.remove(tempDir, { recursive: true }); + } + }); +} + +function testCopySync(name: string, cb: (tempDir: string) => void): void { + test({ + name, + fn: (): void => { + const tempDir = Deno.makeTempDirSync({ + prefix: "deno_std_copy_sync_test_" + }); + cb(tempDir); + Deno.removeSync(tempDir, { recursive: true }); + } + }); +} + +testCopy( + "[fs] copy file if it does no exist", + async (tempDir: string): Promise<void> => { + const srcFile = path.join(testdataDir, "copy_file_not_exists.txt"); + const destFile = path.join(tempDir, "copy_file_not_exists_1.txt"); + await assertThrowsAsync( + async (): Promise<void> => { + await copy(srcFile, destFile); + } + ); + } +); + +testCopy( + "[fs] copy if src and dest are the same paths", + async (tempDir: string): Promise<void> => { + const srcFile = path.join(tempDir, "copy_file_same.txt"); + const destFile = path.join(tempDir, "copy_file_same.txt"); + await assertThrowsAsync( + async (): Promise<void> => { + await copy(srcFile, destFile); + }, + Error, + "Source and destination cannot be the same." + ); + } +); + +testCopy( + "[fs] copy file", + async (tempDir: string): Promise<void> => { + const srcFile = path.join(testdataDir, "copy_file.txt"); + const destFile = path.join(tempDir, "copy_file_copy.txt"); + + const srcContent = new TextDecoder().decode(await Deno.readFile(srcFile)); + + assertEquals( + await exists(srcFile), + true, + `source should exist before copy` + ); + assertEquals( + await exists(destFile), + false, + "destination should not exist before copy" + ); + + await copy(srcFile, destFile); + + assertEquals(await exists(srcFile), true, "source should exist after copy"); + assertEquals( + await exists(destFile), + true, + "destination should exist before copy" + ); + + const destContent = new TextDecoder().decode(await Deno.readFile(destFile)); + + assertEquals( + srcContent, + destContent, + "source and destination should have the same content" + ); + + // Copy again and it should throw an error. + await assertThrowsAsync( + async (): Promise<void> => { + await copy(srcFile, destFile); + }, + Error, + `'${destFile}' already exists.` + ); + + // Modify destination file. + await Deno.writeFile(destFile, new TextEncoder().encode("txt copy")); + + assertEquals( + new TextDecoder().decode(await Deno.readFile(destFile)), + "txt copy" + ); + + // Copy again with overwrite option. + await copy(srcFile, destFile, { overwrite: true }); + + // Make sure the file has been overwritten. + assertEquals( + new TextDecoder().decode(await Deno.readFile(destFile)), + "txt" + ); + } +); + +testCopy( + "[fs] copy with preserve timestamps", + async (tempDir: string): Promise<void> => { + const srcFile = path.join(testdataDir, "copy_file.txt"); + const destFile = path.join(tempDir, "copy_file_copy.txt"); + + const srcStatInfo = await Deno.stat(srcFile); + + assert(typeof srcStatInfo.accessed === "number"); + assert(typeof srcStatInfo.modified === "number"); + + // Copy with overwrite and preserve timestamps options. + await copy(srcFile, destFile, { + overwrite: true, + preserveTimestamps: true + }); + + const destStatInfo = await Deno.stat(destFile); + + assert(typeof destStatInfo.accessed === "number"); + assert(typeof destStatInfo.modified === "number"); + assertEquals(destStatInfo.accessed, srcStatInfo.accessed); + assertEquals(destStatInfo.modified, srcStatInfo.modified); + } +); + +testCopy( + "[fs] copy directory to its subdirectory", + async (tempDir: string): Promise<void> => { + const srcDir = path.join(tempDir, "parent"); + const destDir = path.join(srcDir, "child"); + + await ensureDir(srcDir); + + await assertThrowsAsync( + async (): Promise<void> => { + await copy(srcDir, destDir); + }, + Error, + `Cannot copy '${srcDir}' to a subdirectory of itself, '${destDir}'.` + ); + } +); + +testCopy( + "[fs] copy directory and destination exist and not a directory", + async (tempDir: string): Promise<void> => { + const srcDir = path.join(tempDir, "parent"); + const destDir = path.join(tempDir, "child.txt"); + + await ensureDir(srcDir); + await ensureFile(destDir); + + await assertThrowsAsync( + async (): Promise<void> => { + await copy(srcDir, destDir); + }, + Error, + `Cannot overwrite non-directory '${destDir}' with directory '${srcDir}'.` + ); + } +); + +testCopy( + "[fs] copy directory", + async (tempDir: string): Promise<void> => { + const srcDir = path.join(testdataDir, "copy_dir"); + const destDir = path.join(tempDir, "copy_dir"); + const srcFile = path.join(srcDir, "0.txt"); + const destFile = path.join(destDir, "0.txt"); + const srcNestFile = path.join(srcDir, "nest", "0.txt"); + const destNestFile = path.join(destDir, "nest", "0.txt"); + + await copy(srcDir, destDir); + + assertEquals(await exists(destFile), true); + assertEquals(await exists(destNestFile), true); + + // After copy. The source and destination should have the same content. + assertEquals( + new TextDecoder().decode(await Deno.readFile(srcFile)), + new TextDecoder().decode(await Deno.readFile(destFile)) + ); + assertEquals( + new TextDecoder().decode(await Deno.readFile(srcNestFile)), + new TextDecoder().decode(await Deno.readFile(destNestFile)) + ); + + // Copy again without overwrite option and it should throw an error. + await assertThrowsAsync( + async (): Promise<void> => { + await copy(srcDir, destDir); + }, + Error, + `'${destDir}' already exists.` + ); + + // Modify the file in the destination directory. + await Deno.writeFile(destNestFile, new TextEncoder().encode("nest copy")); + assertEquals( + new TextDecoder().decode(await Deno.readFile(destNestFile)), + "nest copy" + ); + + // Copy again with overwrite option. + await copy(srcDir, destDir, { overwrite: true }); + + // Make sure the file has been overwritten. + assertEquals( + new TextDecoder().decode(await Deno.readFile(destNestFile)), + "nest" + ); + } +); + +testCopy( + "[fs] copy symlink file", + async (tempDir: string): Promise<void> => { + const dir = path.join(testdataDir, "copy_dir_link_file"); + const srcLink = path.join(dir, "0.txt"); + const destLink = path.join(tempDir, "0_copy.txt"); + + if (isWindows) { + await assertThrowsAsync( + // (): Promise<void> => copy(srcLink, destLink), + (): Promise<void> => ensureSymlink(srcLink, destLink) + ); + return; + } + + assert( + (await Deno.lstat(srcLink)).isSymlink(), + `'${srcLink}' should be symlink type` + ); + + await copy(srcLink, destLink); + + const statInfo = await Deno.lstat(destLink); + + assert(statInfo.isSymlink(), `'${destLink}' should be symlink type`); + } +); + +testCopy( + "[fs] copy symlink directory", + async (tempDir: string): Promise<void> => { + const srcDir = path.join(testdataDir, "copy_dir"); + const srcLink = path.join(tempDir, "copy_dir_link"); + const destLink = path.join(tempDir, "copy_dir_link_copy"); + + if (isWindows) { + await assertThrowsAsync( + // (): Promise<void> => copy(srcLink, destLink), + (): Promise<void> => ensureSymlink(srcLink, destLink) + ); + return; + } + + await ensureSymlink(srcDir, srcLink); + + assert( + (await Deno.lstat(srcLink)).isSymlink(), + `'${srcLink}' should be symlink type` + ); + + await copy(srcLink, destLink); + + const statInfo = await Deno.lstat(destLink); + + assert(statInfo.isSymlink()); + } +); + +testCopySync( + "[fs] copy file synchronously if it does not exist", + (tempDir: string): void => { + const srcFile = path.join(testdataDir, "copy_file_not_exists_sync.txt"); + const destFile = path.join(tempDir, "copy_file_not_exists_1_sync.txt"); + assertThrows((): void => { + copySync(srcFile, destFile); + }); + } +); + +testCopySync( + "[fs] copy synchronously with preserve timestamps", + (tempDir: string): void => { + const srcFile = path.join(testdataDir, "copy_file.txt"); + const destFile = path.join(tempDir, "copy_file_copy.txt"); + + const srcStatInfo = Deno.statSync(srcFile); + + assert(typeof srcStatInfo.accessed === "number"); + assert(typeof srcStatInfo.modified === "number"); + + // Copy with overwrite and preserve timestamps options. + copySync(srcFile, destFile, { + overwrite: true, + preserveTimestamps: true + }); + + const destStatInfo = Deno.statSync(destFile); + + assert(typeof destStatInfo.accessed === "number"); + assert(typeof destStatInfo.modified === "number"); + // TODO: Activate test when https://github.com/denoland/deno/issues/2411 + // is fixed + // assertEquals(destStatInfo.accessed, srcStatInfo.accessed); + // assertEquals(destStatInfo.modified, srcStatInfo.modified); + } +); + +testCopySync( + "[fs] copy synchronously if src and dest are the same paths", + (): void => { + const srcFile = path.join(testdataDir, "copy_file_same_sync.txt"); + assertThrows( + (): void => { + copySync(srcFile, srcFile); + }, + Error, + "Source and destination cannot be the same." + ); + } +); + +testCopySync("[fs] copy file synchronously", (tempDir: string): void => { + const srcFile = path.join(testdataDir, "copy_file.txt"); + const destFile = path.join(tempDir, "copy_file_copy_sync.txt"); + + const srcContent = new TextDecoder().decode(Deno.readFileSync(srcFile)); + + assertEquals(existsSync(srcFile), true); + assertEquals(existsSync(destFile), false); + + copySync(srcFile, destFile); + + assertEquals(existsSync(srcFile), true); + assertEquals(existsSync(destFile), true); + + const destContent = new TextDecoder().decode(Deno.readFileSync(destFile)); + + assertEquals(srcContent, destContent); + + // Copy again without overwrite option and it should throw an error. + assertThrows( + (): void => { + copySync(srcFile, destFile); + }, + Error, + `'${destFile}' already exists.` + ); + + // Modify destination file. + Deno.writeFileSync(destFile, new TextEncoder().encode("txt copy")); + + assertEquals( + new TextDecoder().decode(Deno.readFileSync(destFile)), + "txt copy" + ); + + // Copy again with overwrite option. + copySync(srcFile, destFile, { overwrite: true }); + + // Make sure the file has been overwritten. + assertEquals(new TextDecoder().decode(Deno.readFileSync(destFile)), "txt"); +}); + +testCopySync( + "[fs] copy directory synchronously to its subdirectory", + (tempDir: string): void => { + const srcDir = path.join(tempDir, "parent"); + const destDir = path.join(srcDir, "child"); + + ensureDirSync(srcDir); + + assertThrows( + (): void => { + copySync(srcDir, destDir); + }, + Error, + `Cannot copy '${srcDir}' to a subdirectory of itself, '${destDir}'.` + ); + } +); + +testCopySync( + "[fs] copy directory synchronously, and destination exist and not a " + + "directory", + (tempDir: string): void => { + const srcDir = path.join(tempDir, "parent_sync"); + const destDir = path.join(tempDir, "child.txt"); + + ensureDirSync(srcDir); + ensureFileSync(destDir); + + assertThrows( + (): void => { + copySync(srcDir, destDir); + }, + Error, + `Cannot overwrite non-directory '${destDir}' with directory '${srcDir}'.` + ); + } +); + +testCopySync("[fs] copy directory synchronously", (tempDir: string): void => { + const srcDir = path.join(testdataDir, "copy_dir"); + const destDir = path.join(tempDir, "copy_dir_copy_sync"); + const srcFile = path.join(srcDir, "0.txt"); + const destFile = path.join(destDir, "0.txt"); + const srcNestFile = path.join(srcDir, "nest", "0.txt"); + const destNestFile = path.join(destDir, "nest", "0.txt"); + + copySync(srcDir, destDir); + + assertEquals(existsSync(destFile), true); + assertEquals(existsSync(destNestFile), true); + + // After copy. The source and destination should have the same content. + assertEquals( + new TextDecoder().decode(Deno.readFileSync(srcFile)), + new TextDecoder().decode(Deno.readFileSync(destFile)) + ); + assertEquals( + new TextDecoder().decode(Deno.readFileSync(srcNestFile)), + new TextDecoder().decode(Deno.readFileSync(destNestFile)) + ); + + // Copy again without overwrite option and it should throw an error. + assertThrows( + (): void => { + copySync(srcDir, destDir); + }, + Error, + `'${destDir}' already exists.` + ); + + // Modify the file in the destination directory. + Deno.writeFileSync(destNestFile, new TextEncoder().encode("nest copy")); + assertEquals( + new TextDecoder().decode(Deno.readFileSync(destNestFile)), + "nest copy" + ); + + // Copy again with overwrite option. + copySync(srcDir, destDir, { overwrite: true }); + + // Make sure the file has been overwritten. + assertEquals( + new TextDecoder().decode(Deno.readFileSync(destNestFile)), + "nest" + ); +}); + +testCopySync( + "[fs] copy symlink file synchronously", + (tempDir: string): void => { + const dir = path.join(testdataDir, "copy_dir_link_file"); + const srcLink = path.join(dir, "0.txt"); + const destLink = path.join(tempDir, "0_copy.txt"); + + if (isWindows) { + assertThrows( + // (): void => copySync(srcLink, destLink), + (): void => ensureSymlinkSync(srcLink, destLink) + ); + return; + } + + assert( + Deno.lstatSync(srcLink).isSymlink(), + `'${srcLink}' should be symlink type` + ); + + copySync(srcLink, destLink); + + const statInfo = Deno.lstatSync(destLink); + + assert(statInfo.isSymlink(), `'${destLink}' should be symlink type`); + } +); + +testCopySync( + "[fs] copy symlink directory synchronously", + (tempDir: string): void => { + const originDir = path.join(testdataDir, "copy_dir"); + const srcLink = path.join(tempDir, "copy_dir_link"); + const destLink = path.join(tempDir, "copy_dir_link_copy"); + + if (isWindows) { + assertThrows( + // (): void => copySync(srcLink, destLink), + (): void => ensureSymlinkSync(srcLink, destLink) + ); + return; + } + + ensureSymlinkSync(originDir, srcLink); + + assert( + Deno.lstatSync(srcLink).isSymlink(), + `'${srcLink}' should be symlink type` + ); + + copySync(srcLink, destLink); + + const statInfo = Deno.lstatSync(destLink); + + assert(statInfo.isSymlink()); + } +); diff --git a/std/fs/empty_dir.ts b/std/fs/empty_dir.ts new file mode 100644 index 000000000..81bc45839 --- /dev/null +++ b/std/fs/empty_dir.ts @@ -0,0 +1,48 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +/** + * Ensures that a directory is empty. + * Deletes directory contents if the directory is not empty. + * If the directory does not exist, it is created. + * The directory itself is not deleted. + */ +export async function emptyDir(dir: string): Promise<void> { + let items: Deno.FileInfo[] = []; + try { + items = await Deno.readDir(dir); + } catch { + // if not exist. then create it + await Deno.mkdir(dir, true); + return; + } + while (items.length) { + const item = items.shift(); + if (item && item.name) { + const fn = dir + "/" + item.name; + await Deno.remove(fn, { recursive: true }); + } + } +} + +/** + * Ensures that a directory is empty. + * Deletes directory contents if the directory is not empty. + * If the directory does not exist, it is created. + * The directory itself is not deleted. + */ +export function emptyDirSync(dir: string): void { + let items: Deno.FileInfo[] = []; + try { + items = Deno.readDirSync(dir); + } catch { + // if not exist. then create it + Deno.mkdirSync(dir, true); + return; + } + while (items.length) { + const item = items.shift(); + if (item && item.name) { + const fn = dir + "/" + item.name; + Deno.removeSync(fn, { recursive: true }); + } + } +} diff --git a/std/fs/empty_dir_test.ts b/std/fs/empty_dir_test.ts new file mode 100644 index 000000000..80d3a1789 --- /dev/null +++ b/std/fs/empty_dir_test.ts @@ -0,0 +1,125 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrows, + assertThrowsAsync +} from "../testing/asserts.ts"; +import { emptyDir, emptyDirSync } from "./empty_dir.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function emptyDirIfItNotExist(): Promise<void> { + const testDir = path.join(testdataDir, "empty_dir_test_1"); + const testNestDir = path.join(testDir, "nest"); + // empty a dir which not exist. then it will create new one + await emptyDir(testNestDir); + + try { + // check the dir + const stat = await Deno.stat(testNestDir); + assertEquals(stat.isDirectory(), true); + } finally { + // remove the test dir + Deno.remove(testDir, { recursive: true }); + } +}); + +test(function emptyDirSyncIfItNotExist(): void { + const testDir = path.join(testdataDir, "empty_dir_test_2"); + const testNestDir = path.join(testDir, "nest"); + // empty a dir which not exist. then it will create new one + emptyDirSync(testNestDir); + + try { + // check the dir + const stat = Deno.statSync(testNestDir); + assertEquals(stat.isDirectory(), true); + } finally { + // remove the test dir + Deno.remove(testDir, { recursive: true }); + } +}); + +test(async function emptyDirIfItExist(): Promise<void> { + const testDir = path.join(testdataDir, "empty_dir_test_3"); + const testNestDir = path.join(testDir, "nest"); + // create test dir + await emptyDir(testNestDir); + const testDirFile = path.join(testNestDir, "test.ts"); + // create test file in test dir + await Deno.writeFile(testDirFile, new Uint8Array()); + + // before empty: make sure file/directory exist + const beforeFileStat = await Deno.stat(testDirFile); + assertEquals(beforeFileStat.isFile(), true); + + const beforeDirStat = await Deno.stat(testNestDir); + assertEquals(beforeDirStat.isDirectory(), true); + + await emptyDir(testDir); + + // after empty: file/directory have already remove + try { + // test dir still there + const stat = await Deno.stat(testDir); + assertEquals(stat.isDirectory(), true); + + // nest directory have been remove + await assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testNestDir); + } + ); + + // test file have been remove + await assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testDirFile); + } + ); + } finally { + // remote test dir + await Deno.remove(testDir, { recursive: true }); + } +}); + +test(function emptyDirSyncIfItExist(): void { + const testDir = path.join(testdataDir, "empty_dir_test_4"); + const testNestDir = path.join(testDir, "nest"); + // create test dir + emptyDirSync(testNestDir); + const testDirFile = path.join(testNestDir, "test.ts"); + // create test file in test dir + Deno.writeFileSync(testDirFile, new Uint8Array()); + + // before empty: make sure file/directory exist + const beforeFileStat = Deno.statSync(testDirFile); + assertEquals(beforeFileStat.isFile(), true); + + const beforeDirStat = Deno.statSync(testNestDir); + assertEquals(beforeDirStat.isDirectory(), true); + + emptyDirSync(testDir); + + // after empty: file/directory have already remove + try { + // test dir still there + const stat = Deno.statSync(testDir); + assertEquals(stat.isDirectory(), true); + + // nest directory have been remove + assertThrows((): void => { + Deno.statSync(testNestDir); + }); + + // test file have been remove + assertThrows((): void => { + Deno.statSync(testDirFile); + }); + } finally { + // remote test dir + Deno.removeSync(testDir, { recursive: true }); + } +}); diff --git a/std/fs/ensure_dir.ts b/std/fs/ensure_dir.ts new file mode 100644 index 000000000..dfc02f35c --- /dev/null +++ b/std/fs/ensure_dir.ts @@ -0,0 +1,49 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { getFileInfoType } from "./utils.ts"; +/** + * Ensures that the directory exists. + * If the directory structure does not exist, it is created. Like mkdir -p. + */ +export async function ensureDir(dir: string): Promise<void> { + let pathExists = false; + try { + // if dir exists + const stat = await Deno.stat(dir); + pathExists = true; + if (!stat.isDirectory()) { + throw new Error( + `Ensure path exists, expected 'dir', got '${getFileInfoType(stat)}'` + ); + } + } catch (err) { + if (pathExists) { + throw err; + } + // if dir not exists. then create it. + await Deno.mkdir(dir, true); + } +} + +/** + * Ensures that the directory exists. + * If the directory structure does not exist, it is created. Like mkdir -p. + */ +export function ensureDirSync(dir: string): void { + let pathExists = false; + try { + // if dir exists + const stat = Deno.statSync(dir); + pathExists = true; + if (!stat.isDirectory()) { + throw new Error( + `Ensure path exists, expected 'dir', got '${getFileInfoType(stat)}'` + ); + } + } catch (err) { + if (pathExists) { + throw err; + } + // if dir not exists. then create it. + Deno.mkdirSync(dir, true); + } +} diff --git a/std/fs/ensure_dir_test.ts b/std/fs/ensure_dir_test.ts new file mode 100644 index 000000000..affffdbe6 --- /dev/null +++ b/std/fs/ensure_dir_test.ts @@ -0,0 +1,107 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertThrows, assertThrowsAsync } from "../testing/asserts.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import * as path from "./path/mod.ts"; +import { ensureFile, ensureFileSync } from "./ensure_file.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function ensureDirIfItNotExist(): Promise<void> { + const baseDir = path.join(testdataDir, "ensure_dir_not_exist"); + const testDir = path.join(baseDir, "test"); + + await ensureDir(testDir); + + await assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testDir).then((): void => { + throw new Error("test dir should exists."); + }); + } + ); + + await Deno.remove(baseDir, { recursive: true }); +}); + +test(function ensureDirSyncIfItNotExist(): void { + const baseDir = path.join(testdataDir, "ensure_dir_sync_not_exist"); + const testDir = path.join(baseDir, "test"); + + ensureDirSync(testDir); + + Deno.statSync(testDir); + + Deno.removeSync(baseDir, { recursive: true }); +}); + +test(async function ensureDirIfItExist(): Promise<void> { + const baseDir = path.join(testdataDir, "ensure_dir_exist"); + const testDir = path.join(baseDir, "test"); + + // create test directory + await Deno.mkdir(testDir, true); + + await ensureDir(testDir); + + await assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testDir).then((): void => { + throw new Error("test dir should still exists."); + }); + } + ); + + await Deno.remove(baseDir, { recursive: true }); +}); + +test(function ensureDirSyncIfItExist(): void { + const baseDir = path.join(testdataDir, "ensure_dir_sync_exist"); + const testDir = path.join(baseDir, "test"); + + // create test directory + Deno.mkdirSync(testDir, true); + + ensureDirSync(testDir); + + assertThrows((): void => { + Deno.statSync(testDir); + throw new Error("test dir should still exists."); + }); + + Deno.removeSync(baseDir, { recursive: true }); +}); + +test(async function ensureDirIfItAsFile(): Promise<void> { + const baseDir = path.join(testdataDir, "ensure_dir_exist_file"); + const testFile = path.join(baseDir, "test"); + + await ensureFile(testFile); + + await assertThrowsAsync( + async (): Promise<void> => { + await ensureDir(testFile); + }, + Error, + `Ensure path exists, expected 'dir', got 'file'` + ); + + await Deno.remove(baseDir, { recursive: true }); +}); + +test(function ensureDirSyncIfItAsFile(): void { + const baseDir = path.join(testdataDir, "ensure_dir_exist_file_async"); + const testFile = path.join(baseDir, "test"); + + ensureFileSync(testFile); + + assertThrows( + (): void => { + ensureDirSync(testFile); + }, + Error, + `Ensure path exists, expected 'dir', got 'file'` + ); + + Deno.removeSync(baseDir, { recursive: true }); +}); diff --git a/std/fs/ensure_file.ts b/std/fs/ensure_file.ts new file mode 100644 index 000000000..d749b95e4 --- /dev/null +++ b/std/fs/ensure_file.ts @@ -0,0 +1,64 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import * as path from "./path/mod.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import { getFileInfoType } from "./utils.ts"; + +/** + * Ensures that the file exists. + * If the file that is requested to be created is in directories that do not + * exist. + * these directories are created. If the file already exists, + * it is NOTMODIFIED. + */ +export async function ensureFile(filePath: string): Promise<void> { + let pathExists = false; + try { + // if file exists + const stat = await Deno.lstat(filePath); + pathExists = true; + if (!stat.isFile()) { + throw new Error( + `Ensure path exists, expected 'file', got '${getFileInfoType(stat)}'` + ); + } + } catch (err) { + if (pathExists) { + throw err; + } + // if file not exists + // ensure dir exists + await ensureDir(path.dirname(filePath)); + // create file + await Deno.writeFile(filePath, new Uint8Array()); + } +} + +/** + * Ensures that the file exists. + * If the file that is requested to be created is in directories that do not + * exist, + * these directories are created. If the file already exists, + * it is NOT MODIFIED. + */ +export function ensureFileSync(filePath: string): void { + let pathExists = false; + try { + // if file exists + const stat = Deno.statSync(filePath); + pathExists = true; + if (!stat.isFile()) { + throw new Error( + `Ensure path exists, expected 'file', got '${getFileInfoType(stat)}'` + ); + } + } catch (err) { + if (pathExists) { + throw err; + } + // if file not exists + // ensure dir exists + ensureDirSync(path.dirname(filePath)); + // create file + Deno.writeFileSync(filePath, new Uint8Array()); + } +} diff --git a/std/fs/ensure_file_test.ts b/std/fs/ensure_file_test.ts new file mode 100644 index 000000000..56f180786 --- /dev/null +++ b/std/fs/ensure_file_test.ts @@ -0,0 +1,107 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertThrows, assertThrowsAsync } from "../testing/asserts.ts"; +import { ensureFile, ensureFileSync } from "./ensure_file.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function ensureFileIfItNotExist(): Promise<void> { + const testDir = path.join(testdataDir, "ensure_file_1"); + const testFile = path.join(testDir, "test.txt"); + + await ensureFile(testFile); + + await assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testFile).then((): void => { + throw new Error("test file should exists."); + }); + } + ); + + await Deno.remove(testDir, { recursive: true }); +}); + +test(function ensureFileSyncIfItNotExist(): void { + const testDir = path.join(testdataDir, "ensure_file_2"); + const testFile = path.join(testDir, "test.txt"); + + ensureFileSync(testFile); + + assertThrows((): void => { + Deno.statSync(testFile); + throw new Error("test file should exists."); + }); + + Deno.removeSync(testDir, { recursive: true }); +}); + +test(async function ensureFileIfItExist(): Promise<void> { + const testDir = path.join(testdataDir, "ensure_file_3"); + const testFile = path.join(testDir, "test.txt"); + + await Deno.mkdir(testDir, true); + await Deno.writeFile(testFile, new Uint8Array()); + + await ensureFile(testFile); + + await assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testFile).then((): void => { + throw new Error("test file should exists."); + }); + } + ); + + await Deno.remove(testDir, { recursive: true }); +}); + +test(function ensureFileSyncIfItExist(): void { + const testDir = path.join(testdataDir, "ensure_file_4"); + const testFile = path.join(testDir, "test.txt"); + + Deno.mkdirSync(testDir, true); + Deno.writeFileSync(testFile, new Uint8Array()); + + ensureFileSync(testFile); + + assertThrows((): void => { + Deno.statSync(testFile); + throw new Error("test file should exists."); + }); + + Deno.removeSync(testDir, { recursive: true }); +}); + +test(async function ensureFileIfItExistAsDir(): Promise<void> { + const testDir = path.join(testdataDir, "ensure_file_5"); + + await Deno.mkdir(testDir, true); + + await assertThrowsAsync( + async (): Promise<void> => { + await ensureFile(testDir); + }, + Error, + `Ensure path exists, expected 'file', got 'dir'` + ); + + await Deno.remove(testDir, { recursive: true }); +}); + +test(function ensureFileSyncIfItExistAsDir(): void { + const testDir = path.join(testdataDir, "ensure_file_6"); + + Deno.mkdirSync(testDir, true); + + assertThrows( + (): void => { + ensureFileSync(testDir); + }, + Error, + `Ensure path exists, expected 'file', got 'dir'` + ); + + Deno.removeSync(testDir, { recursive: true }); +}); diff --git a/std/fs/ensure_link.ts b/std/fs/ensure_link.ts new file mode 100644 index 000000000..707f2bee9 --- /dev/null +++ b/std/fs/ensure_link.ts @@ -0,0 +1,53 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import * as path from "./path/mod.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import { exists, existsSync } from "./exists.ts"; +import { getFileInfoType } from "./utils.ts"; + +/** + * Ensures that the hard link exists. + * If the directory structure does not exist, it is created. + * + * @param src the source file path. Directory hard links are not allowed. + * @param dest the destination link path + */ +export async function ensureLink(src: string, dest: string): Promise<void> { + if (await exists(dest)) { + const destStatInfo = await Deno.lstat(dest); + const destFilePathType = getFileInfoType(destStatInfo); + if (destFilePathType !== "file") { + throw new Error( + `Ensure path exists, expected 'file', got '${destFilePathType}'` + ); + } + return; + } + + await ensureDir(path.dirname(dest)); + + await Deno.link(src, dest); +} + +/** + * Ensures that the hard link exists. + * If the directory structure does not exist, it is created. + * + * @param src the source file path. Directory hard links are not allowed. + * @param dest the destination link path + */ +export function ensureLinkSync(src: string, dest: string): void { + if (existsSync(dest)) { + const destStatInfo = Deno.lstatSync(dest); + const destFilePathType = getFileInfoType(destStatInfo); + if (destFilePathType !== "file") { + throw new Error( + `Ensure path exists, expected 'file', got '${destFilePathType}'` + ); + } + return; + } + + ensureDirSync(path.dirname(dest)); + + Deno.linkSync(src, dest); +} diff --git a/std/fs/ensure_link_test.ts b/std/fs/ensure_link_test.ts new file mode 100644 index 000000000..6d5758268 --- /dev/null +++ b/std/fs/ensure_link_test.ts @@ -0,0 +1,174 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +// TODO(axetroy): Add test for Windows once symlink is implemented for Windows. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrows, + assertThrowsAsync +} from "../testing/asserts.ts"; +import { ensureLink, ensureLinkSync } from "./ensure_link.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function ensureLinkIfItNotExist(): Promise<void> { + const srcDir = path.join(testdataDir, "ensure_link_1"); + const destDir = path.join(testdataDir, "ensure_link_1_2"); + const testFile = path.join(srcDir, "test.txt"); + const linkFile = path.join(destDir, "link.txt"); + + await assertThrowsAsync( + async (): Promise<void> => { + await ensureLink(testFile, linkFile); + } + ); + + await Deno.remove(destDir, { recursive: true }); +}); + +test(function ensureLinkSyncIfItNotExist(): void { + const testDir = path.join(testdataDir, "ensure_link_2"); + const testFile = path.join(testDir, "test.txt"); + const linkFile = path.join(testDir, "link.txt"); + + assertThrows((): void => { + ensureLinkSync(testFile, linkFile); + }); + + Deno.removeSync(testDir, { recursive: true }); +}); + +test(async function ensureLinkIfItExist(): Promise<void> { + const testDir = path.join(testdataDir, "ensure_link_3"); + const testFile = path.join(testDir, "test.txt"); + const linkFile = path.join(testDir, "link.txt"); + + await Deno.mkdir(testDir, true); + await Deno.writeFile(testFile, new Uint8Array()); + + await ensureLink(testFile, linkFile); + + const srcStat = await Deno.lstat(testFile); + const linkStat = await Deno.lstat(linkFile); + + assertEquals(srcStat.isFile(), true); + assertEquals(linkStat.isFile(), true); + + // har link success. try to change one of them. they should be change both. + + // let's change origin file. + await Deno.writeFile(testFile, new TextEncoder().encode("123")); + + const testFileContent1 = new TextDecoder().decode( + await Deno.readFile(testFile) + ); + const linkFileContent1 = new TextDecoder().decode( + await Deno.readFile(testFile) + ); + + assertEquals(testFileContent1, "123"); + assertEquals(testFileContent1, linkFileContent1); + + // let's change link file. + await Deno.writeFile(testFile, new TextEncoder().encode("abc")); + + const testFileContent2 = new TextDecoder().decode( + await Deno.readFile(testFile) + ); + const linkFileContent2 = new TextDecoder().decode( + await Deno.readFile(testFile) + ); + + assertEquals(testFileContent2, "abc"); + assertEquals(testFileContent2, linkFileContent2); + + await Deno.remove(testDir, { recursive: true }); +}); + +test(function ensureLinkSyncIfItExist(): void { + const testDir = path.join(testdataDir, "ensure_link_4"); + const testFile = path.join(testDir, "test.txt"); + const linkFile = path.join(testDir, "link.txt"); + + Deno.mkdirSync(testDir, true); + Deno.writeFileSync(testFile, new Uint8Array()); + + ensureLinkSync(testFile, linkFile); + + const srcStat = Deno.lstatSync(testFile); + + const linkStat = Deno.lstatSync(linkFile); + + assertEquals(srcStat.isFile(), true); + assertEquals(linkStat.isFile(), true); + + // har link success. try to change one of them. they should be change both. + + // let's change origin file. + Deno.writeFileSync(testFile, new TextEncoder().encode("123")); + + const testFileContent1 = new TextDecoder().decode( + Deno.readFileSync(testFile) + ); + const linkFileContent1 = new TextDecoder().decode( + Deno.readFileSync(testFile) + ); + + assertEquals(testFileContent1, "123"); + assertEquals(testFileContent1, linkFileContent1); + + // let's change link file. + Deno.writeFileSync(testFile, new TextEncoder().encode("abc")); + + const testFileContent2 = new TextDecoder().decode( + Deno.readFileSync(testFile) + ); + const linkFileContent2 = new TextDecoder().decode( + Deno.readFileSync(testFile) + ); + + assertEquals(testFileContent2, "abc"); + assertEquals(testFileContent2, linkFileContent2); + + Deno.removeSync(testDir, { recursive: true }); +}); + +test(async function ensureLinkDirectoryIfItExist(): Promise<void> { + const testDir = path.join(testdataDir, "ensure_link_origin_3"); + const linkDir = path.join(testdataDir, "ensure_link_link_3"); + const testFile = path.join(testDir, "test.txt"); + + await Deno.mkdir(testDir, true); + await Deno.writeFile(testFile, new Uint8Array()); + + await assertThrowsAsync( + async (): Promise<void> => { + await ensureLink(testDir, linkDir); + }, + Deno.DenoError + // "Operation not permitted (os error 1)" // throw an local matching test + // "Access is denied. (os error 5)" // throw in CI + ); + + Deno.removeSync(testDir, { recursive: true }); +}); + +test(function ensureLinkSyncDirectoryIfItExist(): void { + const testDir = path.join(testdataDir, "ensure_link_origin_3"); + const linkDir = path.join(testdataDir, "ensure_link_link_3"); + const testFile = path.join(testDir, "test.txt"); + + Deno.mkdirSync(testDir, true); + Deno.writeFileSync(testFile, new Uint8Array()); + + assertThrows( + (): void => { + ensureLinkSync(testDir, linkDir); + }, + Deno.DenoError + // "Operation not permitted (os error 1)" // throw an local matching test + // "Access is denied. (os error 5)" // throw in CI + ); + + Deno.removeSync(testDir, { recursive: true }); +}); diff --git a/std/fs/ensure_symlink.ts b/std/fs/ensure_symlink.ts new file mode 100644 index 000000000..9b7cc429c --- /dev/null +++ b/std/fs/ensure_symlink.ts @@ -0,0 +1,59 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import * as path from "./path/mod.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import { exists, existsSync } from "./exists.ts"; +import { getFileInfoType } from "./utils.ts"; + +/** + * Ensures that the link exists. + * If the directory structure does not exist, it is created. + * + * @param src the source file path + * @param dest the destination link path + */ +export async function ensureSymlink(src: string, dest: string): Promise<void> { + const srcStatInfo = await Deno.lstat(src); + const srcFilePathType = getFileInfoType(srcStatInfo); + + if (await exists(dest)) { + const destStatInfo = await Deno.lstat(dest); + const destFilePathType = getFileInfoType(destStatInfo); + if (destFilePathType !== "symlink") { + throw new Error( + `Ensure path exists, expected 'symlink', got '${destFilePathType}'` + ); + } + return; + } + + await ensureDir(path.dirname(dest)); + + await Deno.symlink(src, dest, srcFilePathType); +} + +/** + * Ensures that the link exists. + * If the directory structure does not exist, it is created. + * + * @param src the source file path + * @param dest the destination link path + */ +export function ensureSymlinkSync(src: string, dest: string): void { + const srcStatInfo = Deno.lstatSync(src); + const srcFilePathType = getFileInfoType(srcStatInfo); + + if (existsSync(dest)) { + const destStatInfo = Deno.lstatSync(dest); + const destFilePathType = getFileInfoType(destStatInfo); + if (destFilePathType !== "symlink") { + throw new Error( + `Ensure path exists, expected 'symlink', got '${destFilePathType}'` + ); + } + return; + } + + ensureDirSync(path.dirname(dest)); + + Deno.symlinkSync(src, dest, srcFilePathType); +} diff --git a/std/fs/ensure_symlink_test.ts b/std/fs/ensure_symlink_test.ts new file mode 100644 index 000000000..82312b758 --- /dev/null +++ b/std/fs/ensure_symlink_test.ts @@ -0,0 +1,169 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +// TODO(axetroy): Add test for Windows once symlink is implemented for Windows. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrows, + assertThrowsAsync +} from "../testing/asserts.ts"; +import { ensureSymlink, ensureSymlinkSync } from "./ensure_symlink.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); +const isWindows = Deno.build.os === "win"; + +test(async function ensureSymlinkIfItNotExist(): Promise<void> { + const testDir = path.join(testdataDir, "link_file_1"); + const testFile = path.join(testDir, "test.txt"); + + assertThrowsAsync( + async (): Promise<void> => { + await ensureSymlink(testFile, path.join(testDir, "test1.txt")); + } + ); + + assertThrowsAsync( + async (): Promise<void> => { + await Deno.stat(testFile).then((): void => { + throw new Error("test file should exists."); + }); + } + ); +}); + +test(function ensureSymlinkSyncIfItNotExist(): void { + const testDir = path.join(testdataDir, "link_file_2"); + const testFile = path.join(testDir, "test.txt"); + + assertThrows((): void => { + ensureSymlinkSync(testFile, path.join(testDir, "test1.txt")); + }); + + assertThrows((): void => { + Deno.statSync(testFile); + throw new Error("test file should exists."); + }); +}); + +test(async function ensureSymlinkIfItExist(): Promise<void> { + const testDir = path.join(testdataDir, "link_file_3"); + const testFile = path.join(testDir, "test.txt"); + const linkFile = path.join(testDir, "link.txt"); + + await Deno.mkdir(testDir, true); + await Deno.writeFile(testFile, new Uint8Array()); + + if (isWindows) { + await assertThrowsAsync( + (): Promise<void> => ensureSymlink(testFile, linkFile), + Error, + "Not implemented" + ); + await Deno.remove(testDir, { recursive: true }); + return; + } else { + await ensureSymlink(testFile, linkFile); + } + + const srcStat = await Deno.lstat(testFile); + const linkStat = await Deno.lstat(linkFile); + + assertEquals(srcStat.isFile(), true); + assertEquals(linkStat.isSymlink(), true); + + await Deno.remove(testDir, { recursive: true }); +}); + +test(function ensureSymlinkSyncIfItExist(): void { + const testDir = path.join(testdataDir, "link_file_4"); + const testFile = path.join(testDir, "test.txt"); + const linkFile = path.join(testDir, "link.txt"); + + Deno.mkdirSync(testDir, true); + Deno.writeFileSync(testFile, new Uint8Array()); + + if (isWindows) { + assertThrows( + (): void => ensureSymlinkSync(testFile, linkFile), + Error, + "Not implemented" + ); + Deno.removeSync(testDir, { recursive: true }); + return; + } else { + ensureSymlinkSync(testFile, linkFile); + } + + const srcStat = Deno.lstatSync(testFile); + + const linkStat = Deno.lstatSync(linkFile); + + assertEquals(srcStat.isFile(), true); + assertEquals(linkStat.isSymlink(), true); + + Deno.removeSync(testDir, { recursive: true }); +}); + +test(async function ensureSymlinkDirectoryIfItExist(): Promise<void> { + const testDir = path.join(testdataDir, "link_file_origin_3"); + const linkDir = path.join(testdataDir, "link_file_link_3"); + const testFile = path.join(testDir, "test.txt"); + + await Deno.mkdir(testDir, true); + await Deno.writeFile(testFile, new Uint8Array()); + + if (isWindows) { + await assertThrowsAsync( + (): Promise<void> => ensureSymlink(testDir, linkDir), + Error, + "Not implemented" + ); + await Deno.remove(testDir, { recursive: true }); + return; + } else { + await ensureSymlink(testDir, linkDir); + } + + const testDirStat = await Deno.lstat(testDir); + const linkDirStat = await Deno.lstat(linkDir); + const testFileStat = await Deno.lstat(testFile); + + assertEquals(testFileStat.isFile(), true); + assertEquals(testDirStat.isDirectory(), true); + assertEquals(linkDirStat.isSymlink(), true); + + await Deno.remove(linkDir, { recursive: true }); + await Deno.remove(testDir, { recursive: true }); +}); + +test(function ensureSymlinkSyncDirectoryIfItExist(): void { + const testDir = path.join(testdataDir, "link_file_origin_3"); + const linkDir = path.join(testdataDir, "link_file_link_3"); + const testFile = path.join(testDir, "test.txt"); + + Deno.mkdirSync(testDir, true); + Deno.writeFileSync(testFile, new Uint8Array()); + + if (isWindows) { + assertThrows( + (): void => ensureSymlinkSync(testDir, linkDir), + Error, + "Not implemented" + ); + Deno.removeSync(testDir, { recursive: true }); + return; + } else { + ensureSymlinkSync(testDir, linkDir); + } + + const testDirStat = Deno.lstatSync(testDir); + const linkDirStat = Deno.lstatSync(linkDir); + const testFileStat = Deno.lstatSync(testFile); + + assertEquals(testFileStat.isFile(), true); + assertEquals(testDirStat.isDirectory(), true); + assertEquals(linkDirStat.isSymlink(), true); + + Deno.removeSync(linkDir, { recursive: true }); + Deno.removeSync(testDir, { recursive: true }); +}); diff --git a/std/fs/eol.ts b/std/fs/eol.ts new file mode 100644 index 000000000..55d03fa83 --- /dev/null +++ b/std/fs/eol.ts @@ -0,0 +1,31 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +/** EndOfLine character enum */ +export enum EOL { + LF = "\n", + CRLF = "\r\n" +} + +const regDetect = /(?:\r?\n)/g; + +/** + * Detect the EOL character for string input. + * returns null if no newline + */ +export function detect(content: string): EOL | null { + const d = content.match(regDetect); + if (!d || d.length === 0) { + return null; + } + const crlf = d.filter((x: string): boolean => x === EOL.CRLF); + if (crlf.length > 0) { + return EOL.CRLF; + } else { + return EOL.LF; + } +} + +/** Format the file to the targeted EOL */ +export function format(content: string, eol: EOL): string { + return content.replace(regDetect, eol); +} diff --git a/std/fs/eol_test.ts b/std/fs/eol_test.ts new file mode 100644 index 000000000..4669c795a --- /dev/null +++ b/std/fs/eol_test.ts @@ -0,0 +1,55 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { format, detect, EOL } from "./eol.ts"; + +const CRLFinput = "deno\r\nis not\r\nnode"; +const Mixedinput = "deno\nis not\r\nnode"; +const Mixedinput2 = "deno\r\nis not\nnode"; +const LFinput = "deno\nis not\nnode"; +const NoNLinput = "deno is not node"; + +test({ + name: "[EOL] Detect CR LF", + fn(): void { + assertEquals(detect(CRLFinput), EOL.CRLF); + } +}); + +test({ + name: "[EOL] Detect LF", + fn(): void { + assertEquals(detect(LFinput), EOL.LF); + } +}); + +test({ + name: "[EOL] Detect No New Line", + fn(): void { + assertEquals(detect(NoNLinput), null); + } +}); + +test({ + name: "[EOL] Detect Mixed", + fn(): void { + assertEquals(detect(Mixedinput), EOL.CRLF); + assertEquals(detect(Mixedinput2), EOL.CRLF); + } +}); + +test({ + name: "[EOL] Format", + fn(): void { + assertEquals(format(CRLFinput, EOL.LF), LFinput); + assertEquals(format(LFinput, EOL.LF), LFinput); + assertEquals(format(LFinput, EOL.CRLF), CRLFinput); + assertEquals(format(CRLFinput, EOL.CRLF), CRLFinput); + assertEquals(format(CRLFinput, EOL.CRLF), CRLFinput); + assertEquals(format(NoNLinput, EOL.CRLF), NoNLinput); + assertEquals(format(Mixedinput, EOL.CRLF), CRLFinput); + assertEquals(format(Mixedinput, EOL.LF), LFinput); + assertEquals(format(Mixedinput2, EOL.CRLF), CRLFinput); + assertEquals(format(Mixedinput2, EOL.LF), LFinput); + } +}); diff --git a/std/fs/exists.ts b/std/fs/exists.ts new file mode 100644 index 000000000..2c68ec6cf --- /dev/null +++ b/std/fs/exists.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +/** + * Test whether or not the given path exists by checking with the file system + */ +export async function exists(filePath: string): Promise<boolean> { + return Deno.lstat(filePath) + .then((): boolean => true) + .catch((): boolean => false); +} + +/** + * Test whether or not the given path exists by checking with the file system + */ +export function existsSync(filePath: string): boolean { + try { + Deno.lstatSync(filePath); + return true; + } catch { + return false; + } +} diff --git a/std/fs/exists_test.ts b/std/fs/exists_test.ts new file mode 100644 index 000000000..247bb7ed6 --- /dev/null +++ b/std/fs/exists_test.ts @@ -0,0 +1,48 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { exists, existsSync } from "./exists.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function existsFile(): Promise<void> { + assertEquals( + await exists(path.join(testdataDir, "not_exist_file.ts")), + false + ); + assertEquals(await existsSync(path.join(testdataDir, "0.ts")), true); +}); + +test(function existsFileSync(): void { + assertEquals(existsSync(path.join(testdataDir, "not_exist_file.ts")), false); + assertEquals(existsSync(path.join(testdataDir, "0.ts")), true); +}); + +test(async function existsDirectory(): Promise<void> { + assertEquals( + await exists(path.join(testdataDir, "not_exist_directory")), + false + ); + assertEquals(existsSync(testdataDir), true); +}); + +test(function existsDirectorySync(): void { + assertEquals( + existsSync(path.join(testdataDir, "not_exist_directory")), + false + ); + assertEquals(existsSync(testdataDir), true); +}); + +test(function existsLinkSync(): void { + // TODO(axetroy): generate link file use Deno api instead of set a link file + // in repository + assertEquals(existsSync(path.join(testdataDir, "0-link.ts")), true); +}); + +test(async function existsLink(): Promise<void> { + // TODO(axetroy): generate link file use Deno api instead of set a link file + // in repository + assertEquals(await exists(path.join(testdataDir, "0-link.ts")), true); +}); diff --git a/std/fs/glob.ts b/std/fs/glob.ts new file mode 100644 index 000000000..86d83b4f8 --- /dev/null +++ b/std/fs/glob.ts @@ -0,0 +1,361 @@ +import { globrex } from "./globrex.ts"; +import { SEP, SEP_PATTERN, isWindows } from "./path/constants.ts"; +import { isAbsolute, join, normalize } from "./path/mod.ts"; +import { WalkInfo, walk, walkSync } from "./walk.ts"; +const { DenoError, ErrorKind, cwd, stat, statSync } = Deno; +type FileInfo = Deno.FileInfo; + +export interface GlobOptions { + extended?: boolean; + globstar?: boolean; +} + +export interface GlobToRegExpOptions extends GlobOptions { + flags?: string; +} + +/** + * Generate a regex based on glob pattern and options + * This was meant to be using the the `fs.walk` function + * but can be used anywhere else. + * Examples: + * + * Looking for all the `ts` files: + * walkSync(".", { + * match: [globToRegExp("*.ts")] + * }) + * + * Looking for all the `.json` files in any subfolder: + * walkSync(".", { + * match: [globToRegExp(join("a", "**", "*.json"),{ + * flags: "g", + * extended: true, + * globstar: true + * })] + * }) + * + * @param glob - Glob pattern to be used + * @param options - Specific options for the glob pattern + * @returns A RegExp for the glob pattern + */ +export function globToRegExp( + glob: string, + options: GlobToRegExpOptions = {} +): RegExp { + const result = globrex(glob, { ...options, strict: false, filepath: true }); + return result.path!.regex; +} + +/** Test whether the given string is a glob */ +export function isGlob(str: string): boolean { + const chars: Record<string, string> = { "{": "}", "(": ")", "[": "]" }; + /* eslint-disable-next-line max-len */ + const regex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; + + if (str === "") { + return false; + } + + let match: RegExpExecArray | null; + + while ((match = regex.exec(str))) { + if (match[2]) return true; + let idx = match.index + match[0].length; + + // if an open bracket/brace/paren is escaped, + // set the index to the next closing character + const open = match[1]; + const close = open ? chars[open] : null; + if (open && close) { + const n = str.indexOf(close, idx); + if (n !== -1) { + idx = n + 1; + } + } + + str = str.slice(idx); + } + + return false; +} + +/** Like normalize(), but doesn't collapse "**\/.." when `globstar` is true. */ +export function normalizeGlob( + glob: string, + { globstar = false }: GlobOptions = {} +): string { + if (!!glob.match(/\0/g)) { + throw new DenoError( + ErrorKind.InvalidPath, + `Glob contains invalid characters: "${glob}"` + ); + } + if (!globstar) { + return normalize(glob); + } + const s = SEP_PATTERN.source; + const badParentPattern = new RegExp( + `(?<=(${s}|^)\\*\\*${s})\\.\\.(?=${s}|$)`, + "g" + ); + return normalize(glob.replace(badParentPattern, "\0")).replace(/\0/g, ".."); +} + +/** Like join(), but doesn't collapse "**\/.." when `globstar` is true. */ +export function joinGlobs( + globs: string[], + { extended = false, globstar = false }: GlobOptions = {} +): string { + if (!globstar || globs.length == 0) { + return join(...globs); + } + if (globs.length === 0) return "."; + let joined: string | undefined; + for (const glob of globs) { + const path = glob; + if (path.length > 0) { + if (!joined) joined = path; + else joined += `${SEP}${path}`; + } + } + if (!joined) return "."; + return normalizeGlob(joined, { extended, globstar }); +} + +export interface ExpandGlobOptions extends GlobOptions { + root?: string; + exclude?: string[]; + includeDirs?: boolean; +} + +interface SplitPath { + segments: string[]; + isAbsolute: boolean; + hasTrailingSep: boolean; + // Defined for any absolute Windows path. + winRoot?: string; +} + +// TODO: Maybe make this public somewhere. +function split(path: string): SplitPath { + const s = SEP_PATTERN.source; + const segments = path + .replace(new RegExp(`^${s}|${s}$`, "g"), "") + .split(SEP_PATTERN); + const isAbsolute_ = isAbsolute(path); + return { + segments, + isAbsolute: isAbsolute_, + hasTrailingSep: !!path.match(new RegExp(`${s}$`)), + winRoot: isWindows && isAbsolute_ ? segments.shift() : undefined + }; +} + +/** + * Expand the glob string from the specified `root` directory and yield each + * result as a `WalkInfo` object. + */ +// TODO: Use a proper glob expansion algorithm. +// This is a very incomplete solution. The whole directory tree from `root` is +// walked and parent paths are not supported. +export async function* expandGlob( + glob: string, + { + root = cwd(), + exclude = [], + includeDirs = true, + extended = false, + globstar = false + }: ExpandGlobOptions = {} +): AsyncIterableIterator<WalkInfo> { + const globOptions: GlobOptions = { extended, globstar }; + const absRoot = isAbsolute(root) + ? normalize(root) + : joinGlobs([cwd(), root], globOptions); + const resolveFromRoot = (path: string): string => + isAbsolute(path) + ? normalize(path) + : joinGlobs([absRoot, path], globOptions); + const excludePatterns = exclude + .map(resolveFromRoot) + .map((s: string): RegExp => globToRegExp(s, globOptions)); + const shouldInclude = ({ filename }: WalkInfo): boolean => + !excludePatterns.some((p: RegExp): boolean => !!filename.match(p)); + const { segments, hasTrailingSep, winRoot } = split(resolveFromRoot(glob)); + + let fixedRoot = winRoot != undefined ? winRoot : "/"; + while (segments.length > 0 && !isGlob(segments[0])) { + fixedRoot = joinGlobs([fixedRoot, segments.shift()!], globOptions); + } + + let fixedRootInfo: WalkInfo; + try { + fixedRootInfo = { filename: fixedRoot, info: await stat(fixedRoot) }; + } catch { + return; + } + + async function* advanceMatch( + walkInfo: WalkInfo, + globSegment: string + ): AsyncIterableIterator<WalkInfo> { + if (!walkInfo.info.isDirectory()) { + return; + } else if (globSegment == "..") { + const parentPath = joinGlobs([walkInfo.filename, ".."], globOptions); + try { + return yield* [ + { filename: parentPath, info: await stat(parentPath) } + ].filter(shouldInclude); + } catch { + return; + } + } else if (globSegment == "**") { + return yield* walk(walkInfo.filename, { + includeFiles: false, + skip: excludePatterns + }); + } + yield* walk(walkInfo.filename, { + maxDepth: 1, + match: [ + globToRegExp( + joinGlobs([walkInfo.filename, globSegment], globOptions), + globOptions + ) + ], + skip: excludePatterns + }); + } + + let currentMatches: WalkInfo[] = [fixedRootInfo]; + for (const segment of segments) { + // Advancing the list of current matches may introduce duplicates, so we + // pass everything through this Map. + const nextMatchMap: Map<string, FileInfo> = new Map(); + for (const currentMatch of currentMatches) { + for await (const nextMatch of advanceMatch(currentMatch, segment)) { + nextMatchMap.set(nextMatch.filename, nextMatch.info); + } + } + currentMatches = [...nextMatchMap].sort().map( + ([filename, info]): WalkInfo => ({ + filename, + info + }) + ); + } + if (hasTrailingSep) { + currentMatches = currentMatches.filter(({ info }): boolean => + info.isDirectory() + ); + } + if (!includeDirs) { + currentMatches = currentMatches.filter( + ({ info }): boolean => !info.isDirectory() + ); + } + yield* currentMatches; +} + +/** Synchronous version of `expandGlob()`. */ +// TODO: As `expandGlob()`. +export function* expandGlobSync( + glob: string, + { + root = cwd(), + exclude = [], + includeDirs = true, + extended = false, + globstar = false + }: ExpandGlobOptions = {} +): IterableIterator<WalkInfo> { + const globOptions: GlobOptions = { extended, globstar }; + const absRoot = isAbsolute(root) + ? normalize(root) + : joinGlobs([cwd(), root], globOptions); + const resolveFromRoot = (path: string): string => + isAbsolute(path) + ? normalize(path) + : joinGlobs([absRoot, path], globOptions); + const excludePatterns = exclude + .map(resolveFromRoot) + .map((s: string): RegExp => globToRegExp(s, globOptions)); + const shouldInclude = ({ filename }: WalkInfo): boolean => + !excludePatterns.some((p: RegExp): boolean => !!filename.match(p)); + const { segments, hasTrailingSep, winRoot } = split(resolveFromRoot(glob)); + + let fixedRoot = winRoot != undefined ? winRoot : "/"; + while (segments.length > 0 && !isGlob(segments[0])) { + fixedRoot = joinGlobs([fixedRoot, segments.shift()!], globOptions); + } + + let fixedRootInfo: WalkInfo; + try { + fixedRootInfo = { filename: fixedRoot, info: statSync(fixedRoot) }; + } catch { + return; + } + + function* advanceMatch( + walkInfo: WalkInfo, + globSegment: string + ): IterableIterator<WalkInfo> { + if (!walkInfo.info.isDirectory()) { + return; + } else if (globSegment == "..") { + const parentPath = joinGlobs([walkInfo.filename, ".."], globOptions); + try { + return yield* [ + { filename: parentPath, info: statSync(parentPath) } + ].filter(shouldInclude); + } catch { + return; + } + } else if (globSegment == "**") { + return yield* walkSync(walkInfo.filename, { + includeFiles: false, + skip: excludePatterns + }); + } + yield* walkSync(walkInfo.filename, { + maxDepth: 1, + match: [ + globToRegExp( + joinGlobs([walkInfo.filename, globSegment], globOptions), + globOptions + ) + ], + skip: excludePatterns + }); + } + + let currentMatches: WalkInfo[] = [fixedRootInfo]; + for (const segment of segments) { + // Advancing the list of current matches may introduce duplicates, so we + // pass everything through this Map. + const nextMatchMap: Map<string, FileInfo> = new Map(); + for (const currentMatch of currentMatches) { + for (const nextMatch of advanceMatch(currentMatch, segment)) { + nextMatchMap.set(nextMatch.filename, nextMatch.info); + } + } + currentMatches = [...nextMatchMap].sort().map( + ([filename, info]): WalkInfo => ({ + filename, + info + }) + ); + } + if (hasTrailingSep) { + currentMatches = currentMatches.filter(({ info }): boolean => + info.isDirectory() + ); + } + if (!includeDirs) { + currentMatches = currentMatches.filter( + ({ info }): boolean => !info.isDirectory() + ); + } + yield* currentMatches; +} diff --git a/std/fs/glob_test.ts b/std/fs/glob_test.ts new file mode 100644 index 000000000..df819d92c --- /dev/null +++ b/std/fs/glob_test.ts @@ -0,0 +1,374 @@ +const { cwd, mkdir } = Deno; +import { test, runIfMain } from "../testing/mod.ts"; +import { assert, assertEquals } from "../testing/asserts.ts"; +import { SEP, isWindows } from "./path/constants.ts"; +import { + ExpandGlobOptions, + expandGlob, + expandGlobSync, + globToRegExp, + isGlob, + joinGlobs, + normalizeGlob +} from "./glob.ts"; +import { join, normalize, relative } from "./path.ts"; +import { testWalk } from "./walk_test.ts"; +import { touch, walkArray } from "./walk_test.ts"; + +test({ + name: "glob: glob to regex", + fn(): void { + assertEquals(globToRegExp("unicorn.*") instanceof RegExp, true); + assertEquals(globToRegExp("unicorn.*").test("poney.ts"), false); + assertEquals(globToRegExp("unicorn.*").test("unicorn.py"), true); + assertEquals(globToRegExp("*.ts").test("poney.ts"), true); + assertEquals(globToRegExp("*.ts").test("unicorn.js"), false); + assertEquals( + globToRegExp(join("unicorn", "**", "cathedral.ts")).test( + join("unicorn", "in", "the", "cathedral.ts") + ), + true + ); + assertEquals( + globToRegExp(join("unicorn", "**", "cathedral.ts")).test( + join("unicorn", "in", "the", "kitchen.ts") + ), + false + ); + assertEquals( + globToRegExp(join("unicorn", "**", "bathroom.*")).test( + join("unicorn", "sleeping", "in", "bathroom.py") + ), + true + ); + assertEquals( + globToRegExp(join("unicorn", "!(sleeping)", "bathroom.ts"), { + extended: true + }).test(join("unicorn", "flying", "bathroom.ts")), + true + ); + assertEquals( + globToRegExp(join("unicorn", "(!sleeping)", "bathroom.ts"), { + extended: true + }).test(join("unicorn", "sleeping", "bathroom.ts")), + false + ); + } +}); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await touch(d + "/a/x.ts"); + }, + async function globInWalk(): Promise<void> { + const arr = await walkArray(".", { match: [globToRegExp("*.ts")] }); + assertEquals(arr.length, 1); + assertEquals(arr[0], "a/x.ts"); + } +); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await mkdir(d + "/b"); + await touch(d + "/a/x.ts"); + await touch(d + "/b/z.ts"); + await touch(d + "/b/z.js"); + }, + async function globInWalkWildcardFiles(): Promise<void> { + const arr = await walkArray(".", { match: [globToRegExp("*.ts")] }); + assertEquals(arr.length, 2); + assertEquals(arr[0], "a/x.ts"); + assertEquals(arr[1], "b/z.ts"); + } +); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await mkdir(d + "/a/yo"); + await touch(d + "/a/yo/x.ts"); + }, + async function globInWalkFolderWildcard(): Promise<void> { + const arr = await walkArray(".", { + match: [ + globToRegExp(join("a", "**", "*.ts"), { + flags: "g", + globstar: true + }) + ] + }); + assertEquals(arr.length, 1); + assertEquals(arr[0], "a/yo/x.ts"); + } +); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await mkdir(d + "/a/unicorn"); + await mkdir(d + "/a/deno"); + await mkdir(d + "/a/raptor"); + await touch(d + "/a/raptor/x.ts"); + await touch(d + "/a/deno/x.ts"); + await touch(d + "/a/unicorn/x.ts"); + }, + async function globInWalkFolderExtended(): Promise<void> { + const arr = await walkArray(".", { + match: [ + globToRegExp(join("a", "+(raptor|deno)", "*.ts"), { + flags: "g", + extended: true + }) + ] + }); + assertEquals(arr.length, 2); + assertEquals(arr[0], "a/deno/x.ts"); + assertEquals(arr[1], "a/raptor/x.ts"); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x.ts"); + await touch(d + "/x.js"); + await touch(d + "/b.js"); + }, + async function globInWalkWildcardExtension(): Promise<void> { + const arr = await walkArray(".", { + match: [globToRegExp("x.*", { flags: "g", globstar: true })] + }); + assertEquals(arr.length, 2); + assertEquals(arr[0], "x.js"); + assertEquals(arr[1], "x.ts"); + } +); + +test({ + name: "isGlob: pattern to test", + fn(): void { + // should be true if valid glob pattern + assert(isGlob("!foo.js")); + assert(isGlob("*.js")); + assert(isGlob("!*.js")); + assert(isGlob("!foo")); + assert(isGlob("!foo.js")); + assert(isGlob("**/abc.js")); + assert(isGlob("abc/*.js")); + assert(isGlob("@.(?:abc)")); + assert(isGlob("@.(?!abc)")); + + // should be false if invalid glob pattern + assert(!isGlob("")); + assert(!isGlob("~/abc")); + assert(!isGlob("~/abc")); + assert(!isGlob("~/(abc)")); + assert(!isGlob("+~(abc)")); + assert(!isGlob(".")); + assert(!isGlob("@.(abc)")); + assert(!isGlob("aa")); + assert(!isGlob("who?")); + assert(!isGlob("why!?")); + assert(!isGlob("where???")); + assert(!isGlob("abc!/def/!ghi.js")); + assert(!isGlob("abc.js")); + assert(!isGlob("abc/def/!ghi.js")); + assert(!isGlob("abc/def/ghi.js")); + + // Should be true if path has regex capture group + assert(isGlob("abc/(?!foo).js")); + assert(isGlob("abc/(?:foo).js")); + assert(isGlob("abc/(?=foo).js")); + assert(isGlob("abc/(a|b).js")); + assert(isGlob("abc/(a|b|c).js")); + assert(isGlob("abc/(foo bar)/*.js")); + + // Should be false if the path has parens but is not a valid capture group + assert(!isGlob("abc/(?foo).js")); + assert(!isGlob("abc/(a b c).js")); + assert(!isGlob("abc/(ab).js")); + assert(!isGlob("abc/(abc).js")); + assert(!isGlob("abc/(foo bar).js")); + + // should be false if the capture group is imbalanced + assert(!isGlob("abc/(?ab.js")); + assert(!isGlob("abc/(ab.js")); + assert(!isGlob("abc/(a|b.js")); + assert(!isGlob("abc/(a|b|c.js")); + + // should be true if the path has a regex character class + assert(isGlob("abc/[abc].js")); + assert(isGlob("abc/[^abc].js")); + assert(isGlob("abc/[1-3].js")); + + // should be false if the character class is not balanced + assert(!isGlob("abc/[abc.js")); + assert(!isGlob("abc/[^abc.js")); + assert(!isGlob("abc/[1-3.js")); + + // should be false if the character class is escaped + assert(!isGlob("abc/\\[abc].js")); + assert(!isGlob("abc/\\[^abc].js")); + assert(!isGlob("abc/\\[1-3].js")); + + // should be true if the path has brace characters + assert(isGlob("abc/{a,b}.js")); + assert(isGlob("abc/{a..z}.js")); + assert(isGlob("abc/{a..z..2}.js")); + + // should be false if (basic) braces are not balanced + assert(!isGlob("abc/\\{a,b}.js")); + assert(!isGlob("abc/\\{a..z}.js")); + assert(!isGlob("abc/\\{a..z..2}.js")); + + // should be true if the path has regex characters + assert(isGlob("!&(abc)")); + assert(isGlob("!*.js")); + assert(isGlob("!foo")); + assert(isGlob("!foo.js")); + assert(isGlob("**/abc.js")); + assert(isGlob("*.js")); + assert(isGlob("*z(abc)")); + assert(isGlob("[1-10].js")); + assert(isGlob("[^abc].js")); + assert(isGlob("[a-j]*[^c]b/c")); + assert(isGlob("[abc].js")); + assert(isGlob("a/b/c/[a-z].js")); + assert(isGlob("abc/(aaa|bbb).js")); + assert(isGlob("abc/*.js")); + assert(isGlob("abc/{a,b}.js")); + assert(isGlob("abc/{a..z..2}.js")); + assert(isGlob("abc/{a..z}.js")); + + assert(!isGlob("$(abc)")); + assert(!isGlob("&(abc)")); + assert(!isGlob("Who?.js")); + assert(!isGlob("? (abc)")); + assert(!isGlob("?.js")); + assert(!isGlob("abc/?.js")); + + // should be false if regex characters are escaped + assert(!isGlob("\\?.js")); + assert(!isGlob("\\[1-10\\].js")); + assert(!isGlob("\\[^abc\\].js")); + assert(!isGlob("\\[a-j\\]\\*\\[^c\\]b/c")); + assert(!isGlob("\\[abc\\].js")); + assert(!isGlob("\\a/b/c/\\[a-z\\].js")); + assert(!isGlob("abc/\\(aaa|bbb).js")); + assert(!isGlob("abc/\\?.js")); + } +}); + +test(function normalizeGlobGlobstar(): void { + assertEquals(normalizeGlob(`**${SEP}..`, { globstar: true }), `**${SEP}..`); +}); + +test(function joinGlobsGlobstar(): void { + assertEquals(joinGlobs(["**", ".."], { globstar: true }), `**${SEP}..`); +}); + +async function expandGlobArray( + globString: string, + options: ExpandGlobOptions +): Promise<string[]> { + const paths: string[] = []; + for await (const { filename } of expandGlob(globString, options)) { + paths.push(filename); + } + paths.sort(); + const pathsSync = [...expandGlobSync(globString, options)].map( + ({ filename }): string => filename + ); + pathsSync.sort(); + assertEquals(paths, pathsSync); + const root = normalize(options.root || cwd()); + for (const path of paths) { + assert(path.startsWith(root)); + } + const relativePaths = paths.map( + (path: string): string => relative(root, path) || "." + ); + relativePaths.sort(); + return relativePaths; +} + +function urlToFilePath(url: URL): string { + // Since `new URL('file:///C:/a').pathname` is `/C:/a`, remove leading slash. + return url.pathname.slice(url.protocol == "file:" && isWindows ? 1 : 0); +} + +const EG_OPTIONS: ExpandGlobOptions = { + root: urlToFilePath(new URL(join("testdata", "glob"), import.meta.url)), + includeDirs: true, + extended: false, + globstar: false +}; + +test(async function expandGlobWildcard(): Promise<void> { + const options = EG_OPTIONS; + assertEquals(await expandGlobArray("*", options), [ + "abc", + "abcdef", + "abcdefghi", + "subdir" + ]); +}); + +test(async function expandGlobTrailingSeparator(): Promise<void> { + const options = EG_OPTIONS; + assertEquals(await expandGlobArray("*/", options), ["subdir"]); +}); + +test(async function expandGlobParent(): Promise<void> { + const options = EG_OPTIONS; + assertEquals(await expandGlobArray("subdir/../*", options), [ + "abc", + "abcdef", + "abcdefghi", + "subdir" + ]); +}); + +test(async function expandGlobExt(): Promise<void> { + const options = { ...EG_OPTIONS, extended: true }; + assertEquals(await expandGlobArray("abc?(def|ghi)", options), [ + "abc", + "abcdef" + ]); + assertEquals(await expandGlobArray("abc*(def|ghi)", options), [ + "abc", + "abcdef", + "abcdefghi" + ]); + assertEquals(await expandGlobArray("abc+(def|ghi)", options), [ + "abcdef", + "abcdefghi" + ]); + assertEquals(await expandGlobArray("abc@(def|ghi)", options), ["abcdef"]); + assertEquals(await expandGlobArray("abc{def,ghi}", options), ["abcdef"]); + assertEquals(await expandGlobArray("abc!(def|ghi)", options), ["abc"]); +}); + +test(async function expandGlobGlobstar(): Promise<void> { + const options = { ...EG_OPTIONS, globstar: true }; + assertEquals( + await expandGlobArray(joinGlobs(["**", "abc"], options), options), + ["abc", join("subdir", "abc")] + ); +}); + +test(async function expandGlobGlobstarParent(): Promise<void> { + const options = { ...EG_OPTIONS, globstar: true }; + assertEquals( + await expandGlobArray(joinGlobs(["subdir", "**", ".."], options), options), + ["."] + ); +}); + +test(async function expandGlobIncludeDirs(): Promise<void> { + const options = { ...EG_OPTIONS, includeDirs: false }; + assertEquals(await expandGlobArray("subdir", options), []); +}); + +runIfMain(import.meta); diff --git a/std/fs/globrex.ts b/std/fs/globrex.ts new file mode 100644 index 000000000..5bf0e16a0 --- /dev/null +++ b/std/fs/globrex.ts @@ -0,0 +1,326 @@ +// This file is ported from globrex@0.1.2 +// MIT License +// Copyright (c) 2018 Terkel Gjervig Nielsen + +const isWin = Deno.build.os === "win"; +const SEP = isWin ? `(\\\\+|\\/)` : `\\/`; +const SEP_ESC = isWin ? `\\\\` : `/`; +const SEP_RAW = isWin ? `\\` : `/`; +const GLOBSTAR = `((?:[^${SEP_ESC}/]*(?:${SEP_ESC}|\/|$))*)`; +const WILDCARD = `([^${SEP_ESC}/]*)`; +const GLOBSTAR_SEGMENT = `((?:[^${SEP_ESC}/]*(?:${SEP_ESC}|\/|$))*)`; +const WILDCARD_SEGMENT = `([^${SEP_ESC}/]*)`; + +export interface GlobrexOptions { + // Allow ExtGlob features + extended?: boolean; + // When globstar is true, '/foo/**' is equivelant + // to '/foo/*' when globstar is false. + // Having globstar set to true is the same usage as + // using wildcards in bash + globstar?: boolean; + // be laissez faire about mutiple slashes + strict?: boolean; + // Parse as filepath for extra path related features + filepath?: boolean; + // Flag to use in the generated RegExp + flags?: string; +} + +export interface GlobrexResult { + regex: RegExp; + path?: { + regex: RegExp; + segments: RegExp[]; + globstar?: RegExp; + }; +} + +/** + * Convert any glob pattern to a JavaScript Regexp object + * @param glob Glob pattern to convert + * @param opts Configuration object + * @param [opts.extended=false] Support advanced ext globbing + * @param [opts.globstar=false] Support globstar + * @param [opts.strict=true] be laissez faire about mutiple slashes + * @param [opts.filepath=""] Parse as filepath for extra path related features + * @param [opts.flags=""] RegExp globs + * @returns Converted object with string, segments and RegExp object + */ +export function globrex( + glob: string, + { + extended = false, + globstar = false, + strict = false, + filepath = false, + flags = "" + }: GlobrexOptions = {} +): GlobrexResult { + let regex = ""; + let segment = ""; + let pathRegexStr = ""; + const pathSegments = []; + + // If we are doing extended matching, this boolean is true when we are inside + // a group (eg {*.html,*.js}), and false otherwise. + let inGroup = false; + let inRange = false; + + // extglob stack. Keep track of scope + const ext = []; + + interface AddOptions { + split?: boolean; + last?: boolean; + only?: string; + } + + // Helper function to build string and segments + function add( + str: string, + options: AddOptions = { split: false, last: false, only: "" } + ): void { + const { split, last, only } = options; + if (only !== "path") regex += str; + if (filepath && only !== "regex") { + pathRegexStr += str.match(new RegExp(`^${SEP}$`)) ? SEP : str; + if (split) { + if (last) segment += str; + if (segment !== "") { + // change it 'includes' + if (!flags.includes("g")) segment = `^${segment}$`; + pathSegments.push(new RegExp(segment, flags)); + } + segment = ""; + } else { + segment += str; + } + } + } + + let c, n; + for (let i = 0; i < glob.length; i++) { + c = glob[i]; + n = glob[i + 1]; + + if (["\\", "$", "^", ".", "="].includes(c)) { + add(`\\${c}`); + continue; + } + + if (c === "/") { + add(`\\${c}`, { split: true }); + if (n === "/" && !strict) regex += "?"; + continue; + } + + if (c === "(") { + if (ext.length) { + add(c); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === ")") { + if (ext.length) { + add(c); + const type: string | undefined = ext.pop(); + if (type === "@") { + add("{1}"); + } else if (type === "!") { + add("([^/]*)"); + } else { + add(type as string); + } + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "|") { + if (ext.length) { + add(c); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "+") { + if (n === "(" && extended) { + ext.push(c); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "@" && extended) { + if (n === "(") { + ext.push(c); + continue; + } + } + + if (c === "!") { + if (extended) { + if (inRange) { + add("^"); + continue; + } + if (n === "(") { + ext.push(c); + add("(?!"); + i++; + continue; + } + add(`\\${c}`); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "?") { + if (extended) { + if (n === "(") { + ext.push(c); + } else { + add("."); + } + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "[") { + if (inRange && n === ":") { + i++; // skip [ + let value = ""; + while (glob[++i] !== ":") value += glob[i]; + if (value === "alnum") add("(\\w|\\d)"); + else if (value === "space") add("\\s"); + else if (value === "digit") add("\\d"); + i++; // skip last ] + continue; + } + if (extended) { + inRange = true; + add(c); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "]") { + if (extended) { + inRange = false; + add(c); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "{") { + if (extended) { + inGroup = true; + add("("); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "}") { + if (extended) { + inGroup = false; + add(")"); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === ",") { + if (inGroup) { + add("|"); + continue; + } + add(`\\${c}`); + continue; + } + + if (c === "*") { + if (n === "(" && extended) { + ext.push(c); + continue; + } + // Move over all consecutive "*"'s. + // Also store the previous and next characters + const prevChar = glob[i - 1]; + let starCount = 1; + while (glob[i + 1] === "*") { + starCount++; + i++; + } + const nextChar = glob[i + 1]; + if (!globstar) { + // globstar is disabled, so treat any number of "*" as one + add(".*"); + } else { + // globstar is enabled, so determine if this is a globstar segment + const isGlobstar = + starCount > 1 && // multiple "*"'s + // from the start of the segment + [SEP_RAW, "/", undefined].includes(prevChar) && + // to the end of the segment + [SEP_RAW, "/", undefined].includes(nextChar); + if (isGlobstar) { + // it's a globstar, so match zero or more path segments + add(GLOBSTAR, { only: "regex" }); + add(GLOBSTAR_SEGMENT, { only: "path", last: true, split: true }); + i++; // move over the "/" + } else { + // it's not a globstar, so only match one path segment + add(WILDCARD, { only: "regex" }); + add(WILDCARD_SEGMENT, { only: "path" }); + } + } + continue; + } + + add(c); + } + + // When regexp 'g' flag is specified don't + // constrain the regular expression with ^ & $ + if (!flags.includes("g")) { + regex = `^${regex}$`; + segment = `^${segment}$`; + if (filepath) pathRegexStr = `^${pathRegexStr}$`; + } + + const result: GlobrexResult = { regex: new RegExp(regex, flags) }; + + // Push the last segment + if (filepath) { + pathSegments.push(new RegExp(segment, flags)); + result.path = { + regex: new RegExp(pathRegexStr, flags), + segments: pathSegments, + globstar: new RegExp( + !flags.includes("g") ? `^${GLOBSTAR_SEGMENT}$` : GLOBSTAR_SEGMENT, + flags + ) + }; + } + + return result; +} diff --git a/std/fs/globrex_test.ts b/std/fs/globrex_test.ts new file mode 100644 index 000000000..31607216d --- /dev/null +++ b/std/fs/globrex_test.ts @@ -0,0 +1,823 @@ +// This file is ported from globrex@0.1.2 +// MIT License +// Copyright (c) 2018 Terkel Gjervig Nielsen + +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { globrex } from "./globrex.ts"; + +const isWin = Deno.build.os === "win"; +const t = { equal: assertEquals, is: assertEquals }; + +function match( + glob: string, + strUnix: string, + strWin?: string | object, + opts = {} +): boolean { + if (typeof strWin === "object") { + opts = strWin; + strWin = ""; + } + const res = globrex(glob, opts); + return res.regex.test(isWin && strWin ? strWin : strUnix); +} + +test({ + name: "globrex: standard", + fn(): void { + const res = globrex("*.js"); + t.equal(typeof globrex, "function", "constructor is a typeof function"); + t.equal(res instanceof Object, true, "returns object"); + t.equal(res.regex.toString(), "/^.*\\.js$/", "returns regex object"); + } +}); + +test({ + name: "globrex: Standard * matching", + fn(): void { + t.equal(match("*", "foo"), true, "match everything"); + t.equal(match("*", "foo", { flags: "g" }), true, "match everything"); + t.equal(match("f*", "foo"), true, "match the end"); + t.equal(match("f*", "foo", { flags: "g" }), true, "match the end"); + t.equal(match("*o", "foo"), true, "match the start"); + t.equal(match("*o", "foo", { flags: "g" }), true, "match the start"); + t.equal(match("u*orn", "unicorn"), true, "match the middle"); + t.equal( + match("u*orn", "unicorn", { flags: "g" }), + true, + "match the middle" + ); + t.equal(match("ico", "unicorn"), false, "do not match without g"); + t.equal( + match("ico", "unicorn", { flags: "g" }), + true, + 'match anywhere with RegExp "g"' + ); + t.equal(match("u*nicorn", "unicorn"), true, "match zero characters"); + t.equal( + match("u*nicorn", "unicorn", { flags: "g" }), + true, + "match zero characters" + ); + } +}); + +test({ + name: "globrex: advance * matching", + fn(): void { + t.equal( + match("*.min.js", "http://example.com/jquery.min.js", { + globstar: false + }), + true, + "complex match" + ); + t.equal( + match("*.min.*", "http://example.com/jquery.min.js", { globstar: false }), + true, + "complex match" + ); + t.equal( + match("*/js/*.js", "http://example.com/js/jquery.min.js", { + globstar: false + }), + true, + "complex match" + ); + t.equal( + match("*.min.*", "http://example.com/jquery.min.js", { flags: "g" }), + true, + "complex match global" + ); + t.equal( + match("*.min.js", "http://example.com/jquery.min.js", { flags: "g" }), + true, + "complex match global" + ); + t.equal( + match("*/js/*.js", "http://example.com/js/jquery.min.js", { flags: "g" }), + true, + "complex match global" + ); + + const str = "\\/$^+?.()=!|{},[].*"; + t.equal(match(str, str), true, "battle test complex string - strict"); + t.equal( + match(str, str, { flags: "g" }), + true, + "battle test complex string - strict" + ); + + t.equal( + match(".min.", "http://example.com/jquery.min.js"), + false, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("*.min.*", "http://example.com/jquery.min.js"), + true, + 'matches without/with using RegExp "g"' + ); + t.equal( + match(".min.", "http://example.com/jquery.min.js", { flags: "g" }), + true, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("http:", "http://example.com/jquery.min.js"), + false, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("http:*", "http://example.com/jquery.min.js"), + true, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("http:", "http://example.com/jquery.min.js", { flags: "g" }), + true, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("min.js", "http://example.com/jquery.min.js"), + false, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("*.min.js", "http://example.com/jquery.min.js"), + true, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("min.js", "http://example.com/jquery.min.js", { flags: "g" }), + true, + 'matches without/with using RegExp "g"' + ); + t.equal( + match("min", "http://example.com/jquery.min.js", { flags: "g" }), + true, + 'match anywhere (globally) using RegExp "g"' + ); + t.equal( + match("/js/", "http://example.com/js/jquery.min.js", { flags: "g" }), + true, + 'match anywhere (globally) using RegExp "g"' + ); + t.equal(match("/js*jq*.js", "http://example.com/js/jquery.min.js"), false); + t.equal( + match("/js*jq*.js", "http://example.com/js/jquery.min.js", { + flags: "g" + }), + true + ); + } +}); + +test({ + name: "globrex: ? match one character, no more and no less", + fn(): void { + t.equal(match("f?o", "foo", { extended: true }), true); + t.equal(match("f?o", "fooo", { extended: true }), false); + t.equal(match("f?oo", "foo", { extended: true }), false); + + const tester = (globstar: boolean): void => { + t.equal( + match("f?o", "foo", { extended: true, globstar, flags: "g" }), + true + ); + t.equal( + match("f?o", "fooo", { extended: true, globstar, flags: "g" }), + true + ); + t.equal( + match("f?o?", "fooo", { extended: true, globstar, flags: "g" }), + true + ); + + t.equal( + match("?fo", "fooo", { extended: true, globstar, flags: "g" }), + false + ); + t.equal( + match("f?oo", "foo", { extended: true, globstar, flags: "g" }), + false + ); + t.equal( + match("foo?", "foo", { extended: true, globstar, flags: "g" }), + false + ); + }; + + tester(true); + tester(false); + } +}); + +test({ + name: "globrex: [] match a character range", + fn(): void { + t.equal(match("fo[oz]", "foo", { extended: true }), true); + t.equal(match("fo[oz]", "foz", { extended: true }), true); + t.equal(match("fo[oz]", "fog", { extended: true }), false); + t.equal(match("fo[a-z]", "fob", { extended: true }), true); + t.equal(match("fo[a-d]", "fot", { extended: true }), false); + t.equal(match("fo[!tz]", "fot", { extended: true }), false); + t.equal(match("fo[!tz]", "fob", { extended: true }), true); + + const tester = (globstar: boolean): void => { + t.equal( + match("fo[oz]", "foo", { extended: true, globstar, flags: "g" }), + true + ); + t.equal( + match("fo[oz]", "foz", { extended: true, globstar, flags: "g" }), + true + ); + t.equal( + match("fo[oz]", "fog", { extended: true, globstar, flags: "g" }), + false + ); + }; + + tester(true); + tester(false); + } +}); + +test({ + name: "globrex: [] extended character ranges", + fn(): void { + t.equal( + match("[[:alnum:]]/bar.txt", "a/bar.txt", { extended: true }), + true + ); + t.equal( + match("@([[:alnum:]abc]|11)/bar.txt", "11/bar.txt", { extended: true }), + true + ); + t.equal( + match("@([[:alnum:]abc]|11)/bar.txt", "a/bar.txt", { extended: true }), + true + ); + t.equal( + match("@([[:alnum:]abc]|11)/bar.txt", "b/bar.txt", { extended: true }), + true + ); + t.equal( + match("@([[:alnum:]abc]|11)/bar.txt", "c/bar.txt", { extended: true }), + true + ); + t.equal( + match("@([[:alnum:]abc]|11)/bar.txt", "abc/bar.txt", { extended: true }), + false + ); + t.equal( + match("@([[:alnum:]abc]|11)/bar.txt", "3/bar.txt", { extended: true }), + true + ); + t.equal( + match("[[:digit:]]/bar.txt", "1/bar.txt", { extended: true }), + true + ); + t.equal( + match("[[:digit:]b]/bar.txt", "b/bar.txt", { extended: true }), + true + ); + t.equal( + match("[![:digit:]b]/bar.txt", "a/bar.txt", { extended: true }), + true + ); + t.equal( + match("[[:alnum:]]/bar.txt", "!/bar.txt", { extended: true }), + false + ); + t.equal( + match("[[:digit:]]/bar.txt", "a/bar.txt", { extended: true }), + false + ); + t.equal( + match("[[:digit:]b]/bar.txt", "a/bar.txt", { extended: true }), + false + ); + } +}); + +test({ + name: "globrex: {} match a choice of different substrings", + fn(): void { + t.equal(match("foo{bar,baaz}", "foobaaz", { extended: true }), true); + t.equal(match("foo{bar,baaz}", "foobar", { extended: true }), true); + t.equal(match("foo{bar,baaz}", "foobuzz", { extended: true }), false); + t.equal(match("foo{bar,b*z}", "foobuzz", { extended: true }), true); + + const tester = (globstar: boolean): void => { + t.equal( + match("foo{bar,baaz}", "foobaaz", { + extended: true, + globstar, + flag: "g" + }), + true + ); + t.equal( + match("foo{bar,baaz}", "foobar", { + extended: true, + globstar, + flag: "g" + }), + true + ); + t.equal( + match("foo{bar,baaz}", "foobuzz", { + extended: true, + globstar, + flag: "g" + }), + false + ); + t.equal( + match("foo{bar,b*z}", "foobuzz", { + extended: true, + globstar, + flag: "g" + }), + true + ); + }; + + tester(true); + tester(false); + } +}); + +test({ + name: "globrex: complex extended matches", + fn(): void { + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://foo.baaz.com/jquery.min.js", + { extended: true } + ), + true + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://moz.buzz.com/index.html", + { extended: true } + ), + true + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://moz.buzz.com/index.htm", + { extended: true } + ), + false + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://moz.bar.com/index.html", + { extended: true } + ), + false + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://flozz.buzz.com/index.html", + { extended: true } + ), + false + ); + + const tester = (globstar: boolean): void => { + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://foo.baaz.com/jquery.min.js", + { extended: true, globstar, flags: "g" } + ), + true + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://moz.buzz.com/index.html", + { extended: true, globstar, flags: "g" } + ), + true + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://moz.buzz.com/index.htm", + { extended: true, globstar, flags: "g" } + ), + false + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://moz.bar.com/index.html", + { extended: true, globstar, flags: "g" } + ), + false + ); + t.equal( + match( + "http://?o[oz].b*z.com/{*.js,*.html}", + "http://flozz.buzz.com/index.html", + { extended: true, globstar, flags: "g" } + ), + false + ); + }; + + tester(true); + tester(false); + } +}); + +test({ + name: "globrex: standard globstar", + fn(): void { + const tester = (globstar: boolean): void => { + t.equal( + match( + "http://foo.com/**/{*.js,*.html}", + "http://foo.com/bar/jquery.min.js", + { extended: true, globstar, flags: "g" } + ), + true + ); + t.equal( + match( + "http://foo.com/**/{*.js,*.html}", + "http://foo.com/bar/baz/jquery.min.js", + { extended: true, globstar, flags: "g" } + ), + true + ); + t.equal( + match("http://foo.com/**", "http://foo.com/bar/baz/jquery.min.js", { + extended: true, + globstar, + flags: "g" + }), + true + ); + }; + + tester(true); + tester(false); + } +}); + +test({ + name: "globrex: remaining chars should match themself", + fn(): void { + const tester = (globstar: boolean): void => { + const testExtStr = "\\/$^+.()=!|,.*"; + t.equal(match(testExtStr, testExtStr, { extended: true }), true); + t.equal( + match(testExtStr, testExtStr, { extended: true, globstar, flags: "g" }), + true + ); + }; + + tester(true); + tester(false); + } +}); + +test({ + name: "globrex: globstar advance testing", + fn(): void { + t.equal(match("/foo/*", "/foo/bar.txt", { globstar: true }), true); + t.equal(match("/foo/**", "/foo/bar.txt", { globstar: true }), true); + t.equal(match("/foo/**", "/foo/bar/baz.txt", { globstar: true }), true); + t.equal(match("/foo/**", "/foo/bar/baz.txt", { globstar: true }), true); + t.equal( + match("/foo/*/*.txt", "/foo/bar/baz.txt", { globstar: true }), + true + ); + t.equal( + match("/foo/**/*.txt", "/foo/bar/baz.txt", { globstar: true }), + true + ); + t.equal( + match("/foo/**/*.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + true + ); + t.equal(match("/foo/**/bar.txt", "/foo/bar.txt", { globstar: true }), true); + t.equal( + match("/foo/**/**/bar.txt", "/foo/bar.txt", { globstar: true }), + true + ); + t.equal( + match("/foo/**/*/baz.txt", "/foo/bar/baz.txt", { globstar: true }), + true + ); + t.equal(match("/foo/**/*.txt", "/foo/bar.txt", { globstar: true }), true); + t.equal( + match("/foo/**/**/*.txt", "/foo/bar.txt", { globstar: true }), + true + ); + t.equal( + match("/foo/**/*/*.txt", "/foo/bar/baz.txt", { globstar: true }), + true + ); + t.equal( + match("**/*.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + true + ); + t.equal(match("**/foo.txt", "foo.txt", { globstar: true }), true); + t.equal(match("**/*.txt", "foo.txt", { globstar: true }), true); + t.equal(match("/foo/*", "/foo/bar/baz.txt", { globstar: true }), false); + t.equal(match("/foo/*.txt", "/foo/bar/baz.txt", { globstar: true }), false); + t.equal( + match("/foo/*/*.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + false + ); + t.equal(match("/foo/*/bar.txt", "/foo/bar.txt", { globstar: true }), false); + t.equal( + match("/foo/*/*/baz.txt", "/foo/bar/baz.txt", { globstar: true }), + false + ); + t.equal( + match("/foo/**.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + false + ); + t.equal( + match("/foo/bar**/*.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + false + ); + t.equal(match("/foo/bar**", "/foo/bar/baz.txt", { globstar: true }), false); + t.equal( + match("**/.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + false + ); + t.equal( + match("*/*.txt", "/foo/bar/baz/qux.txt", { globstar: true }), + false + ); + t.equal(match("*/*.txt", "foo.txt", { globstar: true }), false); + t.equal( + match("http://foo.com/*", "http://foo.com/bar/baz/jquery.min.js", { + extended: true, + globstar: true + }), + false + ); + t.equal( + match("http://foo.com/*", "http://foo.com/bar/baz/jquery.min.js", { + globstar: true + }), + false + ); + t.equal( + match("http://foo.com/*", "http://foo.com/bar/baz/jquery.min.js", { + globstar: false + }), + true + ); + t.equal( + match("http://foo.com/**", "http://foo.com/bar/baz/jquery.min.js", { + globstar: true + }), + true + ); + t.equal( + match( + "http://foo.com/*/*/jquery.min.js", + "http://foo.com/bar/baz/jquery.min.js", + { globstar: true } + ), + true + ); + t.equal( + match( + "http://foo.com/**/jquery.min.js", + "http://foo.com/bar/baz/jquery.min.js", + { globstar: true } + ), + true + ); + t.equal( + match( + "http://foo.com/*/*/jquery.min.js", + "http://foo.com/bar/baz/jquery.min.js", + { globstar: false } + ), + true + ); + t.equal( + match( + "http://foo.com/*/jquery.min.js", + "http://foo.com/bar/baz/jquery.min.js", + { globstar: false } + ), + true + ); + t.equal( + match( + "http://foo.com/*/jquery.min.js", + "http://foo.com/bar/baz/jquery.min.js", + { globstar: true } + ), + false + ); + } +}); + +test({ + name: "globrex: extended extglob ?", + fn(): void { + t.equal(match("(foo).txt", "(foo).txt", { extended: true }), true); + t.equal(match("?(foo).txt", "foo.txt", { extended: true }), true); + t.equal(match("?(foo).txt", ".txt", { extended: true }), true); + t.equal(match("?(foo|bar)baz.txt", "foobaz.txt", { extended: true }), true); + t.equal( + match("?(ba[zr]|qux)baz.txt", "bazbaz.txt", { extended: true }), + true + ); + t.equal( + match("?(ba[zr]|qux)baz.txt", "barbaz.txt", { extended: true }), + true + ); + t.equal( + match("?(ba[zr]|qux)baz.txt", "quxbaz.txt", { extended: true }), + true + ); + t.equal( + match("?(ba[!zr]|qux)baz.txt", "batbaz.txt", { extended: true }), + true + ); + t.equal(match("?(ba*|qux)baz.txt", "batbaz.txt", { extended: true }), true); + t.equal( + match("?(ba*|qux)baz.txt", "batttbaz.txt", { extended: true }), + true + ); + t.equal(match("?(ba*|qux)baz.txt", "quxbaz.txt", { extended: true }), true); + t.equal( + match("?(ba?(z|r)|qux)baz.txt", "bazbaz.txt", { extended: true }), + true + ); + t.equal( + match("?(ba?(z|?(r))|qux)baz.txt", "bazbaz.txt", { extended: true }), + true + ); + t.equal(match("?(foo).txt", "foo.txt", { extended: false }), false); + t.equal( + match("?(foo|bar)baz.txt", "foobarbaz.txt", { extended: true }), + false + ); + t.equal( + match("?(ba[zr]|qux)baz.txt", "bazquxbaz.txt", { extended: true }), + false + ); + t.equal( + match("?(ba[!zr]|qux)baz.txt", "bazbaz.txt", { extended: true }), + false + ); + } +}); + +test({ + name: "globrex: extended extglob *", + fn(): void { + t.equal(match("*(foo).txt", "foo.txt", { extended: true }), true); + t.equal(match("*foo.txt", "bofoo.txt", { extended: true }), true); + t.equal(match("*(foo).txt", "foofoo.txt", { extended: true }), true); + t.equal(match("*(foo).txt", ".txt", { extended: true }), true); + t.equal(match("*(fooo).txt", ".txt", { extended: true }), true); + t.equal(match("*(fooo).txt", "foo.txt", { extended: true }), false); + t.equal(match("*(foo|bar).txt", "foobar.txt", { extended: true }), true); + t.equal(match("*(foo|bar).txt", "barbar.txt", { extended: true }), true); + t.equal(match("*(foo|bar).txt", "barfoobar.txt", { extended: true }), true); + t.equal(match("*(foo|bar).txt", ".txt", { extended: true }), true); + t.equal(match("*(foo|ba[rt]).txt", "bat.txt", { extended: true }), true); + t.equal(match("*(foo|b*[rt]).txt", "blat.txt", { extended: true }), true); + t.equal(match("*(foo|b*[rt]).txt", "tlat.txt", { extended: true }), false); + t.equal( + match("*(*).txt", "whatever.txt", { extended: true, globstar: true }), + true + ); + t.equal( + match("*(foo|bar)/**/*.txt", "foo/hello/world/bar.txt", { + extended: true, + globstar: true + }), + true + ); + t.equal( + match("*(foo|bar)/**/*.txt", "foo/world/bar.txt", { + extended: true, + globstar: true + }), + true + ); + } +}); + +test({ + name: "globrex: extended extglob +", + fn(): void { + t.equal(match("+(foo).txt", "foo.txt", { extended: true }), true); + t.equal(match("+foo.txt", "+foo.txt", { extended: true }), true); + t.equal(match("+(foo).txt", ".txt", { extended: true }), false); + t.equal(match("+(foo|bar).txt", "foobar.txt", { extended: true }), true); + } +}); + +test({ + name: "globrex: extended extglob @", + fn(): void { + t.equal(match("@(foo).txt", "foo.txt", { extended: true }), true); + t.equal(match("@foo.txt", "@foo.txt", { extended: true }), true); + t.equal(match("@(foo|baz)bar.txt", "foobar.txt", { extended: true }), true); + t.equal( + match("@(foo|baz)bar.txt", "foobazbar.txt", { extended: true }), + false + ); + t.equal( + match("@(foo|baz)bar.txt", "foofoobar.txt", { extended: true }), + false + ); + t.equal( + match("@(foo|baz)bar.txt", "toofoobar.txt", { extended: true }), + false + ); + } +}); + +test({ + name: "globrex: extended extglob !", + fn(): void { + t.equal(match("!(boo).txt", "foo.txt", { extended: true }), true); + t.equal(match("!(foo|baz)bar.txt", "buzbar.txt", { extended: true }), true); + t.equal(match("!bar.txt", "!bar.txt", { extended: true }), true); + t.equal( + match("!({foo,bar})baz.txt", "notbaz.txt", { extended: true }), + true + ); + t.equal( + match("!({foo,bar})baz.txt", "foobaz.txt", { extended: true }), + false + ); + } +}); + +test({ + name: "globrex: strict", + fn(): void { + t.equal(match("foo//bar.txt", "foo/bar.txt"), true); + t.equal(match("foo///bar.txt", "foo/bar.txt"), true); + t.equal(match("foo///bar.txt", "foo/bar.txt", { strict: true }), false); + } +}); + +test({ + name: "globrex: stress testing", + fn(): void { + t.equal( + match("**/*/?yfile.{md,js,txt}", "foo/bar/baz/myfile.md", { + extended: true + }), + true + ); + t.equal( + match("**/*/?yfile.{md,js,txt}", "foo/baz/myfile.md", { extended: true }), + true + ); + t.equal( + match("**/*/?yfile.{md,js,txt}", "foo/baz/tyfile.js", { extended: true }), + true + ); + t.equal( + match("[[:digit:]_.]/file.js", "1/file.js", { extended: true }), + true + ); + t.equal( + match("[[:digit:]_.]/file.js", "2/file.js", { extended: true }), + true + ); + t.equal( + match("[[:digit:]_.]/file.js", "_/file.js", { extended: true }), + true + ); + t.equal( + match("[[:digit:]_.]/file.js", "./file.js", { extended: true }), + true + ); + t.equal( + match("[[:digit:]_.]/file.js", "z/file.js", { extended: true }), + false + ); + } +}); diff --git a/std/fs/mod.ts b/std/fs/mod.ts new file mode 100644 index 000000000..edbd7009f --- /dev/null +++ b/std/fs/mod.ts @@ -0,0 +1,17 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +export * from "./empty_dir.ts"; +export * from "./ensure_dir.ts"; +export * from "./ensure_file.ts"; +export * from "./ensure_link.ts"; +export * from "./ensure_symlink.ts"; +export * from "./exists.ts"; +export * from "./glob.ts"; +export * from "./globrex.ts"; +export * from "./move.ts"; +export * from "./copy.ts"; +export * from "./read_file_str.ts"; +export * from "./write_file_str.ts"; +export * from "./read_json.ts"; +export * from "./write_json.ts"; +export * from "./walk.ts"; +export * from "./eol.ts"; diff --git a/std/fs/move.ts b/std/fs/move.ts new file mode 100644 index 000000000..190f88609 --- /dev/null +++ b/std/fs/move.ts @@ -0,0 +1,59 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { exists, existsSync } from "./exists.ts"; +import { isSubdir } from "./utils.ts"; + +interface MoveOptions { + overwrite?: boolean; +} + +/** Moves a file or directory */ +export async function move( + src: string, + dest: string, + options?: MoveOptions +): Promise<void> { + const srcStat = await Deno.stat(src); + + if (srcStat.isDirectory() && isSubdir(src, dest)) { + throw new Error( + `Cannot move '${src}' to a subdirectory of itself, '${dest}'.` + ); + } + + if (options && options.overwrite) { + await Deno.remove(dest, { recursive: true }); + await Deno.rename(src, dest); + } else { + if (await exists(dest)) { + throw new Error("dest already exists."); + } + await Deno.rename(src, dest); + } + + return; +} + +/** Moves a file or directory */ +export function moveSync( + src: string, + dest: string, + options?: MoveOptions +): void { + const srcStat = Deno.statSync(src); + + if (srcStat.isDirectory() && isSubdir(src, dest)) { + throw new Error( + `Cannot move '${src}' to a subdirectory of itself, '${dest}'.` + ); + } + + if (options && options.overwrite) { + Deno.removeSync(dest, { recursive: true }); + Deno.renameSync(src, dest); + } else { + if (existsSync(dest)) { + throw new Error("dest already exists."); + } + Deno.renameSync(src, dest); + } +} diff --git a/std/fs/move_test.ts b/std/fs/move_test.ts new file mode 100644 index 000000000..bc73784b2 --- /dev/null +++ b/std/fs/move_test.ts @@ -0,0 +1,330 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrows, + assertThrowsAsync +} from "../testing/asserts.ts"; +import { move, moveSync } from "./move.ts"; +import { ensureFile, ensureFileSync } from "./ensure_file.ts"; +import { ensureDir, ensureDirSync } from "./ensure_dir.ts"; +import { exists, existsSync } from "./exists.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function moveDirectoryIfSrcNotExists(): Promise<void> { + const srcDir = path.join(testdataDir, "move_test_src_1"); + const destDir = path.join(testdataDir, "move_test_dest_1"); + // if src directory not exist + await assertThrowsAsync( + async (): Promise<void> => { + await move(srcDir, destDir); + } + ); +}); + +test(async function moveDirectoryIfDestNotExists(): Promise<void> { + const srcDir = path.join(testdataDir, "move_test_src_2"); + const destDir = path.join(testdataDir, "move_test_dest_2"); + + await Deno.mkdir(srcDir, true); + + // if dest directory not exist + await assertThrowsAsync( + async (): Promise<void> => { + await move(srcDir, destDir); + throw new Error("should not throw error"); + }, + Error, + "should not throw error" + ); + + await Deno.remove(destDir); +}); + +test(async function moveFileIfSrcNotExists(): Promise<void> { + const srcFile = path.join(testdataDir, "move_test_src_3", "test.txt"); + const destFile = path.join(testdataDir, "move_test_dest_3", "test.txt"); + + // if src directory not exist + await assertThrowsAsync( + async (): Promise<void> => { + await move(srcFile, destFile); + } + ); +}); + +test(async function moveFileIfDestExists(): Promise<void> { + const srcDir = path.join(testdataDir, "move_test_src_4"); + const destDir = path.join(testdataDir, "move_test_dest_4"); + const srcFile = path.join(srcDir, "test.txt"); + const destFile = path.join(destDir, "test.txt"); + const srcContent = new TextEncoder().encode("src"); + const destContent = new TextEncoder().encode("dest"); + + // make sure files exists + await Promise.all([ensureFile(srcFile), ensureFile(destFile)]); + + // write file content + await Promise.all([ + Deno.writeFile(srcFile, srcContent), + Deno.writeFile(destFile, destContent) + ]); + + // make sure the test file have been created + assertEquals(new TextDecoder().decode(await Deno.readFile(srcFile)), "src"); + assertEquals(new TextDecoder().decode(await Deno.readFile(destFile)), "dest"); + + // move it without override + await assertThrowsAsync( + async (): Promise<void> => { + await move(srcFile, destFile); + }, + Error, + "dest already exists" + ); + + // move again with overwrite + await assertThrowsAsync( + async (): Promise<void> => { + await move(srcFile, destFile, { overwrite: true }); + throw new Error("should not throw error"); + }, + Error, + "should not throw error" + ); + + assertEquals(await exists(srcFile), false); + assertEquals(new TextDecoder().decode(await Deno.readFile(destFile)), "src"); + + // clean up + await Promise.all([ + Deno.remove(srcDir, { recursive: true }), + Deno.remove(destDir, { recursive: true }) + ]); +}); + +test(async function moveDirectory(): Promise<void> { + const srcDir = path.join(testdataDir, "move_test_src_5"); + const destDir = path.join(testdataDir, "move_test_dest_5"); + const srcFile = path.join(srcDir, "test.txt"); + const destFile = path.join(destDir, "test.txt"); + const srcContent = new TextEncoder().encode("src"); + + await Deno.mkdir(srcDir, true); + assertEquals(await exists(srcDir), true); + await Deno.writeFile(srcFile, srcContent); + + await move(srcDir, destDir); + + assertEquals(await exists(srcDir), false); + assertEquals(await exists(destDir), true); + assertEquals(await exists(destFile), true); + + const destFileContent = new TextDecoder().decode( + await Deno.readFile(destFile) + ); + assertEquals(destFileContent, "src"); + + await Deno.remove(destDir, { recursive: true }); +}); + +test(async function moveIfSrcAndDestDirectoryExistsAndOverwrite(): Promise< + void +> { + const srcDir = path.join(testdataDir, "move_test_src_6"); + const destDir = path.join(testdataDir, "move_test_dest_6"); + const srcFile = path.join(srcDir, "test.txt"); + const destFile = path.join(destDir, "test.txt"); + const srcContent = new TextEncoder().encode("src"); + const destContent = new TextEncoder().encode("dest"); + + await Promise.all([Deno.mkdir(srcDir, true), Deno.mkdir(destDir, true)]); + assertEquals(await exists(srcDir), true); + assertEquals(await exists(destDir), true); + await Promise.all([ + Deno.writeFile(srcFile, srcContent), + Deno.writeFile(destFile, destContent) + ]); + + await move(srcDir, destDir, { overwrite: true }); + + assertEquals(await exists(srcDir), false); + assertEquals(await exists(destDir), true); + assertEquals(await exists(destFile), true); + + const destFileContent = new TextDecoder().decode( + await Deno.readFile(destFile) + ); + assertEquals(destFileContent, "src"); + + await Deno.remove(destDir, { recursive: true }); +}); + +test(async function moveIntoSubDir(): Promise<void> { + const srcDir = path.join(testdataDir, "move_test_src_7"); + const destDir = path.join(srcDir, "nest"); + + await ensureDir(destDir); + + await assertThrowsAsync( + async (): Promise<void> => { + await move(srcDir, destDir); + }, + Error, + `Cannot move '${srcDir}' to a subdirectory of itself, '${destDir}'.` + ); + await Deno.remove(srcDir, { recursive: true }); +}); + +test(function moveSyncDirectoryIfSrcNotExists(): void { + const srcDir = path.join(testdataDir, "move_sync_test_src_1"); + const destDir = path.join(testdataDir, "move_sync_test_dest_1"); + // if src directory not exist + assertThrows((): void => { + moveSync(srcDir, destDir); + }); +}); + +test(function moveSyncDirectoryIfDestNotExists(): void { + const srcDir = path.join(testdataDir, "move_sync_test_src_2"); + const destDir = path.join(testdataDir, "move_sync_test_dest_2"); + + Deno.mkdirSync(srcDir, true); + + // if dest directory not exist + assertThrows( + (): void => { + moveSync(srcDir, destDir); + throw new Error("should not throw error"); + }, + Error, + "should not throw error" + ); + + Deno.removeSync(destDir); +}); + +test(function moveSyncFileIfSrcNotExists(): void { + const srcFile = path.join(testdataDir, "move_sync_test_src_3", "test.txt"); + const destFile = path.join(testdataDir, "move_sync_test_dest_3", "test.txt"); + + // if src directory not exist + assertThrows((): void => { + moveSync(srcFile, destFile); + }); +}); + +test(function moveSyncFileIfDestExists(): void { + const srcDir = path.join(testdataDir, "move_sync_test_src_4"); + const destDir = path.join(testdataDir, "move_sync_test_dest_4"); + const srcFile = path.join(srcDir, "test.txt"); + const destFile = path.join(destDir, "test.txt"); + const srcContent = new TextEncoder().encode("src"); + const destContent = new TextEncoder().encode("dest"); + + // make sure files exists + ensureFileSync(srcFile); + ensureFileSync(destFile); + + // write file content + Deno.writeFileSync(srcFile, srcContent); + Deno.writeFileSync(destFile, destContent); + + // make sure the test file have been created + assertEquals(new TextDecoder().decode(Deno.readFileSync(srcFile)), "src"); + assertEquals(new TextDecoder().decode(Deno.readFileSync(destFile)), "dest"); + + // move it without override + assertThrows( + (): void => { + moveSync(srcFile, destFile); + }, + Error, + "dest already exists" + ); + + // move again with overwrite + assertThrows( + (): void => { + moveSync(srcFile, destFile, { overwrite: true }); + throw new Error("should not throw error"); + }, + Error, + "should not throw error" + ); + + assertEquals(existsSync(srcFile), false); + assertEquals(new TextDecoder().decode(Deno.readFileSync(destFile)), "src"); + + // clean up + Deno.removeSync(srcDir, { recursive: true }); + Deno.removeSync(destDir, { recursive: true }); +}); + +test(function moveSyncDirectory(): void { + const srcDir = path.join(testdataDir, "move_sync_test_src_5"); + const destDir = path.join(testdataDir, "move_sync_test_dest_5"); + const srcFile = path.join(srcDir, "test.txt"); + const destFile = path.join(destDir, "test.txt"); + const srcContent = new TextEncoder().encode("src"); + + Deno.mkdirSync(srcDir, true); + assertEquals(existsSync(srcDir), true); + Deno.writeFileSync(srcFile, srcContent); + + moveSync(srcDir, destDir); + + assertEquals(existsSync(srcDir), false); + assertEquals(existsSync(destDir), true); + assertEquals(existsSync(destFile), true); + + const destFileContent = new TextDecoder().decode(Deno.readFileSync(destFile)); + assertEquals(destFileContent, "src"); + + Deno.removeSync(destDir, { recursive: true }); +}); + +test(function moveSyncIfSrcAndDestDirectoryExistsAndOverwrite(): void { + const srcDir = path.join(testdataDir, "move_sync_test_src_6"); + const destDir = path.join(testdataDir, "move_sync_test_dest_6"); + const srcFile = path.join(srcDir, "test.txt"); + const destFile = path.join(destDir, "test.txt"); + const srcContent = new TextEncoder().encode("src"); + const destContent = new TextEncoder().encode("dest"); + + Deno.mkdirSync(srcDir, true); + Deno.mkdirSync(destDir, true); + assertEquals(existsSync(srcDir), true); + assertEquals(existsSync(destDir), true); + Deno.writeFileSync(srcFile, srcContent); + Deno.writeFileSync(destFile, destContent); + + moveSync(srcDir, destDir, { overwrite: true }); + + assertEquals(existsSync(srcDir), false); + assertEquals(existsSync(destDir), true); + assertEquals(existsSync(destFile), true); + + const destFileContent = new TextDecoder().decode(Deno.readFileSync(destFile)); + assertEquals(destFileContent, "src"); + + Deno.removeSync(destDir, { recursive: true }); +}); + +test(function moveSyncIntoSubDir(): void { + const srcDir = path.join(testdataDir, "move_sync_test_src_7"); + const destDir = path.join(srcDir, "nest"); + + ensureDirSync(destDir); + + assertThrows( + (): void => { + moveSync(srcDir, destDir); + }, + Error, + `Cannot move '${srcDir}' to a subdirectory of itself, '${destDir}'.` + ); + Deno.removeSync(srcDir, { recursive: true }); +}); diff --git a/std/fs/path.ts b/std/fs/path.ts new file mode 100644 index 000000000..6ca0749c2 --- /dev/null +++ b/std/fs/path.ts @@ -0,0 +1,3 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +export * from "./path/mod.ts"; +export * from "./path/interface.ts"; diff --git a/std/fs/path/README.md b/std/fs/path/README.md new file mode 100644 index 000000000..93d268aa7 --- /dev/null +++ b/std/fs/path/README.md @@ -0,0 +1,7 @@ +# Deno Path Manipulation Libraries + +Usage: + +```ts +import * as path from "https://deno.land/std/fs/path.ts"; +``` diff --git a/std/fs/path/basename_test.ts b/std/fs/path/basename_test.ts new file mode 100644 index 000000000..f7770d1ca --- /dev/null +++ b/std/fs/path/basename_test.ts @@ -0,0 +1,76 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +test(function basename() { + assertEquals(path.basename(".js", ".js"), ""); + assertEquals(path.basename(""), ""); + assertEquals(path.basename("/dir/basename.ext"), "basename.ext"); + assertEquals(path.basename("/basename.ext"), "basename.ext"); + assertEquals(path.basename("basename.ext"), "basename.ext"); + assertEquals(path.basename("basename.ext/"), "basename.ext"); + assertEquals(path.basename("basename.ext//"), "basename.ext"); + assertEquals(path.basename("aaa/bbb", "/bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb", "a/bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb", "bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb//", "bbb"), "bbb"); + assertEquals(path.basename("aaa/bbb", "bb"), "b"); + assertEquals(path.basename("aaa/bbb", "b"), "bb"); + assertEquals(path.basename("/aaa/bbb", "/bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb", "a/bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb", "bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb//", "bbb"), "bbb"); + assertEquals(path.basename("/aaa/bbb", "bb"), "b"); + assertEquals(path.basename("/aaa/bbb", "b"), "bb"); + assertEquals(path.basename("/aaa/bbb"), "bbb"); + assertEquals(path.basename("/aaa/"), "aaa"); + assertEquals(path.basename("/aaa/b"), "b"); + assertEquals(path.basename("/a/b"), "b"); + assertEquals(path.basename("//a"), "a"); + + // On unix a backslash is just treated as any other character. + assertEquals( + path.posix.basename("\\dir\\basename.ext"), + "\\dir\\basename.ext" + ); + assertEquals(path.posix.basename("\\basename.ext"), "\\basename.ext"); + assertEquals(path.posix.basename("basename.ext"), "basename.ext"); + assertEquals(path.posix.basename("basename.ext\\"), "basename.ext\\"); + assertEquals(path.posix.basename("basename.ext\\\\"), "basename.ext\\\\"); + assertEquals(path.posix.basename("foo"), "foo"); + + // POSIX filenames may include control characters + const controlCharFilename = "Icon" + String.fromCharCode(13); + assertEquals( + path.posix.basename("/a/b/" + controlCharFilename), + controlCharFilename + ); +}); + +test(function basenameWin32() { + assertEquals(path.win32.basename("\\dir\\basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("\\basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("basename.ext\\"), "basename.ext"); + assertEquals(path.win32.basename("basename.ext\\\\"), "basename.ext"); + assertEquals(path.win32.basename("foo"), "foo"); + assertEquals(path.win32.basename("aaa\\bbb", "\\bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb", "a\\bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb", "bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb\\\\\\\\", "bbb"), "bbb"); + assertEquals(path.win32.basename("aaa\\bbb", "bb"), "b"); + assertEquals(path.win32.basename("aaa\\bbb", "b"), "bb"); + assertEquals(path.win32.basename("C:"), ""); + assertEquals(path.win32.basename("C:."), "."); + assertEquals(path.win32.basename("C:\\"), ""); + assertEquals(path.win32.basename("C:\\dir\\base.ext"), "base.ext"); + assertEquals(path.win32.basename("C:\\basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("C:basename.ext"), "basename.ext"); + assertEquals(path.win32.basename("C:basename.ext\\"), "basename.ext"); + assertEquals(path.win32.basename("C:basename.ext\\\\"), "basename.ext"); + assertEquals(path.win32.basename("C:foo"), "foo"); + assertEquals(path.win32.basename("file:stream"), "file:stream"); +}); diff --git a/std/fs/path/constants.ts b/std/fs/path/constants.ts new file mode 100644 index 000000000..1e1eeeb49 --- /dev/null +++ b/std/fs/path/constants.ts @@ -0,0 +1,54 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { build } = Deno; + +// Alphabet chars. +export const CHAR_UPPERCASE_A = 65; /* A */ +export const CHAR_LOWERCASE_A = 97; /* a */ +export const CHAR_UPPERCASE_Z = 90; /* Z */ +export const CHAR_LOWERCASE_Z = 122; /* z */ + +// Non-alphabetic chars. +export const CHAR_DOT = 46; /* . */ +export const CHAR_FORWARD_SLASH = 47; /* / */ +export const CHAR_BACKWARD_SLASH = 92; /* \ */ +export const CHAR_VERTICAL_LINE = 124; /* | */ +export const CHAR_COLON = 58; /* : */ +export const CHAR_QUESTION_MARK = 63; /* ? */ +export const CHAR_UNDERSCORE = 95; /* _ */ +export const CHAR_LINE_FEED = 10; /* \n */ +export const CHAR_CARRIAGE_RETURN = 13; /* \r */ +export const CHAR_TAB = 9; /* \t */ +export const CHAR_FORM_FEED = 12; /* \f */ +export const CHAR_EXCLAMATION_MARK = 33; /* ! */ +export const CHAR_HASH = 35; /* # */ +export const CHAR_SPACE = 32; /* */ +export const CHAR_NO_BREAK_SPACE = 160; /* \u00A0 */ +export const CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279; /* \uFEFF */ +export const CHAR_LEFT_SQUARE_BRACKET = 91; /* [ */ +export const CHAR_RIGHT_SQUARE_BRACKET = 93; /* ] */ +export const CHAR_LEFT_ANGLE_BRACKET = 60; /* < */ +export const CHAR_RIGHT_ANGLE_BRACKET = 62; /* > */ +export const CHAR_LEFT_CURLY_BRACKET = 123; /* { */ +export const CHAR_RIGHT_CURLY_BRACKET = 125; /* } */ +export const CHAR_HYPHEN_MINUS = 45; /* - */ +export const CHAR_PLUS = 43; /* + */ +export const CHAR_DOUBLE_QUOTE = 34; /* " */ +export const CHAR_SINGLE_QUOTE = 39; /* ' */ +export const CHAR_PERCENT = 37; /* % */ +export const CHAR_SEMICOLON = 59; /* ; */ +export const CHAR_CIRCUMFLEX_ACCENT = 94; /* ^ */ +export const CHAR_GRAVE_ACCENT = 96; /* ` */ +export const CHAR_AT = 64; /* @ */ +export const CHAR_AMPERSAND = 38; /* & */ +export const CHAR_EQUAL = 61; /* = */ + +// Digits +export const CHAR_0 = 48; /* 0 */ +export const CHAR_9 = 57; /* 9 */ + +export const isWindows = build.os === "win"; +export const EOL = isWindows ? "\r\n" : "\n"; +export const SEP = isWindows ? "\\" : "/"; +export const SEP_PATTERN = isWindows ? /[\\/]+/ : /\/+/; diff --git a/std/fs/path/dirname_test.ts b/std/fs/path/dirname_test.ts new file mode 100644 index 000000000..047d4859b --- /dev/null +++ b/std/fs/path/dirname_test.ts @@ -0,0 +1,62 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +test(function dirname() { + assertEquals(path.posix.dirname("/a/b/"), "/a"); + assertEquals(path.posix.dirname("/a/b"), "/a"); + assertEquals(path.posix.dirname("/a"), "/"); + assertEquals(path.posix.dirname(""), "."); + assertEquals(path.posix.dirname("/"), "/"); + assertEquals(path.posix.dirname("////"), "/"); + assertEquals(path.posix.dirname("//a"), "//"); + assertEquals(path.posix.dirname("foo"), "."); +}); + +test(function dirnameWin32() { + assertEquals(path.win32.dirname("c:\\"), "c:\\"); + assertEquals(path.win32.dirname("c:\\foo"), "c:\\"); + assertEquals(path.win32.dirname("c:\\foo\\"), "c:\\"); + assertEquals(path.win32.dirname("c:\\foo\\bar"), "c:\\foo"); + assertEquals(path.win32.dirname("c:\\foo\\bar\\"), "c:\\foo"); + assertEquals(path.win32.dirname("c:\\foo\\bar\\baz"), "c:\\foo\\bar"); + assertEquals(path.win32.dirname("\\"), "\\"); + assertEquals(path.win32.dirname("\\foo"), "\\"); + assertEquals(path.win32.dirname("\\foo\\"), "\\"); + assertEquals(path.win32.dirname("\\foo\\bar"), "\\foo"); + assertEquals(path.win32.dirname("\\foo\\bar\\"), "\\foo"); + assertEquals(path.win32.dirname("\\foo\\bar\\baz"), "\\foo\\bar"); + assertEquals(path.win32.dirname("c:"), "c:"); + assertEquals(path.win32.dirname("c:foo"), "c:"); + assertEquals(path.win32.dirname("c:foo\\"), "c:"); + assertEquals(path.win32.dirname("c:foo\\bar"), "c:foo"); + assertEquals(path.win32.dirname("c:foo\\bar\\"), "c:foo"); + assertEquals(path.win32.dirname("c:foo\\bar\\baz"), "c:foo\\bar"); + assertEquals(path.win32.dirname("file:stream"), "."); + assertEquals(path.win32.dirname("dir\\file:stream"), "dir"); + assertEquals(path.win32.dirname("\\\\unc\\share"), "\\\\unc\\share"); + assertEquals(path.win32.dirname("\\\\unc\\share\\foo"), "\\\\unc\\share\\"); + assertEquals(path.win32.dirname("\\\\unc\\share\\foo\\"), "\\\\unc\\share\\"); + assertEquals( + path.win32.dirname("\\\\unc\\share\\foo\\bar"), + "\\\\unc\\share\\foo" + ); + assertEquals( + path.win32.dirname("\\\\unc\\share\\foo\\bar\\"), + "\\\\unc\\share\\foo" + ); + assertEquals( + path.win32.dirname("\\\\unc\\share\\foo\\bar\\baz"), + "\\\\unc\\share\\foo\\bar" + ); + assertEquals(path.win32.dirname("/a/b/"), "/a"); + assertEquals(path.win32.dirname("/a/b"), "/a"); + assertEquals(path.win32.dirname("/a"), "/"); + assertEquals(path.win32.dirname(""), "."); + assertEquals(path.win32.dirname("/"), "/"); + assertEquals(path.win32.dirname("////"), "/"); + assertEquals(path.win32.dirname("foo"), "."); +}); diff --git a/std/fs/path/extname_test.ts b/std/fs/path/extname_test.ts new file mode 100644 index 000000000..336d6b0b2 --- /dev/null +++ b/std/fs/path/extname_test.ts @@ -0,0 +1,90 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const slashRE = /\//g; + +const pairs = [ + ["", ""], + ["/path/to/file", ""], + ["/path/to/file.ext", ".ext"], + ["/path.to/file.ext", ".ext"], + ["/path.to/file", ""], + ["/path.to/.file", ""], + ["/path.to/.file.ext", ".ext"], + ["/path/to/f.ext", ".ext"], + ["/path/to/..ext", ".ext"], + ["/path/to/..", ""], + ["file", ""], + ["file.ext", ".ext"], + [".file", ""], + [".file.ext", ".ext"], + ["/file", ""], + ["/file.ext", ".ext"], + ["/.file", ""], + ["/.file.ext", ".ext"], + [".path/file.ext", ".ext"], + ["file.ext.ext", ".ext"], + ["file.", "."], + [".", ""], + ["./", ""], + [".file.ext", ".ext"], + [".file", ""], + [".file.", "."], + [".file..", "."], + ["..", ""], + ["../", ""], + ["..file.ext", ".ext"], + ["..file", ".file"], + ["..file.", "."], + ["..file..", "."], + ["...", "."], + ["...ext", ".ext"], + ["....", "."], + ["file.ext/", ".ext"], + ["file.ext//", ".ext"], + ["file/", ""], + ["file//", ""], + ["file./", "."], + ["file.//", "."] +]; + +test(function extname() { + pairs.forEach(function(p) { + const input = p[0]; + const expected = p[1]; + assertEquals(expected, path.posix.extname(input)); + }); + + // On *nix, backslash is a valid name component like any other character. + assertEquals(path.posix.extname(".\\"), ""); + assertEquals(path.posix.extname("..\\"), ".\\"); + assertEquals(path.posix.extname("file.ext\\"), ".ext\\"); + assertEquals(path.posix.extname("file.ext\\\\"), ".ext\\\\"); + assertEquals(path.posix.extname("file\\"), ""); + assertEquals(path.posix.extname("file\\\\"), ""); + assertEquals(path.posix.extname("file.\\"), ".\\"); + assertEquals(path.posix.extname("file.\\\\"), ".\\\\"); +}); + +test(function extnameWin32() { + pairs.forEach(function(p) { + const input = p[0].replace(slashRE, "\\"); + const expected = p[1]; + assertEquals(expected, path.win32.extname(input)); + assertEquals(expected, path.win32.extname("C:" + input)); + }); + + // On Windows, backslash is a path separator. + assertEquals(path.win32.extname(".\\"), ""); + assertEquals(path.win32.extname("..\\"), ""); + assertEquals(path.win32.extname("file.ext\\"), ".ext"); + assertEquals(path.win32.extname("file.ext\\\\"), ".ext"); + assertEquals(path.win32.extname("file\\"), ""); + assertEquals(path.win32.extname("file\\\\"), ""); + assertEquals(path.win32.extname("file.\\"), "."); + assertEquals(path.win32.extname("file.\\\\"), "."); +}); diff --git a/std/fs/path/interface.ts b/std/fs/path/interface.ts new file mode 100644 index 000000000..b31c89ea7 --- /dev/null +++ b/std/fs/path/interface.ts @@ -0,0 +1,27 @@ +/** + * A parsed path object generated by path.parse() or consumed by path.format(). + */ +export interface ParsedPath { + /** + * The root of the path such as '/' or 'c:\' + */ + root: string; + /** + * The full directory path such as '/home/user/dir' or 'c:\path\dir' + */ + dir: string; + /** + * The file name including extension (if any) such as 'index.html' + */ + base: string; + /** + * The file extension (if any) such as '.html' + */ + ext: string; + /** + * The file name without extension (if any) such as 'index' + */ + name: string; +} + +export type FormatInputPathObject = Partial<ParsedPath>; diff --git a/std/fs/path/isabsolute_test.ts b/std/fs/path/isabsolute_test.ts new file mode 100644 index 000000000..87218a185 --- /dev/null +++ b/std/fs/path/isabsolute_test.ts @@ -0,0 +1,34 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +test(function isAbsolute() { + assertEquals(path.posix.isAbsolute("/home/foo"), true); + assertEquals(path.posix.isAbsolute("/home/foo/.."), true); + assertEquals(path.posix.isAbsolute("bar/"), false); + assertEquals(path.posix.isAbsolute("./baz"), false); +}); + +test(function isAbsoluteWin32() { + assertEquals(path.win32.isAbsolute("/"), true); + assertEquals(path.win32.isAbsolute("//"), true); + assertEquals(path.win32.isAbsolute("//server"), true); + assertEquals(path.win32.isAbsolute("//server/file"), true); + assertEquals(path.win32.isAbsolute("\\\\server\\file"), true); + assertEquals(path.win32.isAbsolute("\\\\server"), true); + assertEquals(path.win32.isAbsolute("\\\\"), true); + assertEquals(path.win32.isAbsolute("c"), false); + assertEquals(path.win32.isAbsolute("c:"), false); + assertEquals(path.win32.isAbsolute("c:\\"), true); + assertEquals(path.win32.isAbsolute("c:/"), true); + assertEquals(path.win32.isAbsolute("c://"), true); + assertEquals(path.win32.isAbsolute("C:/Users/"), true); + assertEquals(path.win32.isAbsolute("C:\\Users\\"), true); + assertEquals(path.win32.isAbsolute("C:cwd/another"), false); + assertEquals(path.win32.isAbsolute("C:cwd\\another"), false); + assertEquals(path.win32.isAbsolute("directory/directory"), false); + assertEquals(path.win32.isAbsolute("directory\\directory"), false); +}); diff --git a/std/fs/path/join_test.ts b/std/fs/path/join_test.ts new file mode 100644 index 000000000..2c0f35e48 --- /dev/null +++ b/std/fs/path/join_test.ts @@ -0,0 +1,128 @@ +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const backslashRE = /\\/g; + +const joinTests = + // arguments result + [ + [[".", "x/b", "..", "/b/c.js"], "x/b/c.js"], + [[], "."], + [["/.", "x/b", "..", "/b/c.js"], "/x/b/c.js"], + [["/foo", "../../../bar"], "/bar"], + [["foo", "../../../bar"], "../../bar"], + [["foo/", "../../../bar"], "../../bar"], + [["foo/x", "../../../bar"], "../bar"], + [["foo/x", "./bar"], "foo/x/bar"], + [["foo/x/", "./bar"], "foo/x/bar"], + [["foo/x/", ".", "bar"], "foo/x/bar"], + [["./"], "./"], + [[".", "./"], "./"], + [[".", ".", "."], "."], + [[".", "./", "."], "."], + [[".", "/./", "."], "."], + [[".", "/////./", "."], "."], + [["."], "."], + [["", "."], "."], + [["", "foo"], "foo"], + [["foo", "/bar"], "foo/bar"], + [["", "/foo"], "/foo"], + [["", "", "/foo"], "/foo"], + [["", "", "foo"], "foo"], + [["foo", ""], "foo"], + [["foo/", ""], "foo/"], + [["foo", "", "/bar"], "foo/bar"], + [["./", "..", "/foo"], "../foo"], + [["./", "..", "..", "/foo"], "../../foo"], + [[".", "..", "..", "/foo"], "../../foo"], + [["", "..", "..", "/foo"], "../../foo"], + [["/"], "/"], + [["/", "."], "/"], + [["/", ".."], "/"], + [["/", "..", ".."], "/"], + [[""], "."], + [["", ""], "."], + [[" /foo"], " /foo"], + [[" ", "foo"], " /foo"], + [[" ", "."], " "], + [[" ", "/"], " /"], + [[" ", ""], " "], + [["/", "foo"], "/foo"], + [["/", "/foo"], "/foo"], + [["/", "//foo"], "/foo"], + [["/", "", "/foo"], "/foo"], + [["", "/", "foo"], "/foo"], + [["", "/", "/foo"], "/foo"] + ]; + +// Windows-specific join tests +const windowsJoinTests = [ + // arguments result + // UNC path expected + [["//foo/bar"], "\\\\foo\\bar\\"], + [["\\/foo/bar"], "\\\\foo\\bar\\"], + [["\\\\foo/bar"], "\\\\foo\\bar\\"], + // UNC path expected - server and share separate + [["//foo", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "bar"], "\\\\foo\\bar\\"], + [["//foo", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - questionable + [["//foo", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - even more questionable + [["", "//foo", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "/bar"], "\\\\foo\\bar\\"], + // No UNC path expected (no double slash in first component) + [["\\", "foo/bar"], "\\foo\\bar"], + [["\\", "/foo/bar"], "\\foo\\bar"], + [["", "/", "/foo/bar"], "\\foo\\bar"], + // No UNC path expected (no non-slashes in first component - + // questionable) + [["//", "foo/bar"], "\\foo\\bar"], + [["//", "/foo/bar"], "\\foo\\bar"], + [["\\\\", "/", "/foo/bar"], "\\foo\\bar"], + [["//"], "\\"], + // No UNC path expected (share name missing - questionable). + [["//foo"], "\\foo"], + [["//foo/"], "\\foo\\"], + [["//foo", "/"], "\\foo\\"], + [["//foo", "", "/"], "\\foo\\"], + // No UNC path expected (too many leading slashes - questionable) + [["///foo/bar"], "\\foo\\bar"], + [["////foo", "bar"], "\\foo\\bar"], + [["\\\\\\/foo/bar"], "\\foo\\bar"], + // Drive-relative vs drive-absolute paths. This merely describes the + // status quo, rather than being obviously right + [["c:"], "c:."], + [["c:."], "c:."], + [["c:", ""], "c:."], + [["", "c:"], "c:."], + [["c:.", "/"], "c:.\\"], + [["c:.", "file"], "c:file"], + [["c:", "/"], "c:\\"], + [["c:", "file"], "c:\\file"] +]; + +test(function join() { + joinTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.posix.join.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); + +test(function joinWin32() { + joinTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.win32.join.apply(null, _p).replace(backslashRE, "/"); + assertEquals(actual, p[1]); + }); + windowsJoinTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.win32.join.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); diff --git a/std/fs/path/mod.ts b/std/fs/path/mod.ts new file mode 100644 index 000000000..660c061d6 --- /dev/null +++ b/std/fs/path/mod.ts @@ -0,0 +1,25 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import * as _win32 from "./win32.ts"; +import * as _posix from "./posix.ts"; + +import { isWindows } from "./constants.ts"; + +const path = isWindows ? _win32 : _posix; + +export const win32 = _win32; +export const posix = _posix; +export const resolve = path.resolve; +export const normalize = path.normalize; +export const isAbsolute = path.isAbsolute; +export const join = path.join; +export const relative = path.relative; +export const toNamespacedPath = path.toNamespacedPath; +export const dirname = path.dirname; +export const basename = path.basename; +export const extname = path.extname; +export const format = path.format; +export const parse = path.parse; +export const sep = path.sep; +export const delimiter = path.delimiter; diff --git a/std/fs/path/parse_format_test.ts b/std/fs/path/parse_format_test.ts new file mode 100644 index 000000000..db83c3354 --- /dev/null +++ b/std/fs/path/parse_format_test.ts @@ -0,0 +1,180 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// TODO(kt3k): fix any types in this file + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const winPaths = [ + // [path, root] + ["C:\\path\\dir\\index.html", "C:\\"], + ["C:\\another_path\\DIR\\1\\2\\33\\\\index", "C:\\"], + ["another_path\\DIR with spaces\\1\\2\\33\\index", ""], + ["\\", "\\"], + ["\\foo\\C:", "\\"], + ["file", ""], + ["file:stream", ""], + [".\\file", ""], + ["C:", "C:"], + ["C:.", "C:"], + ["C:..", "C:"], + ["C:abc", "C:"], + ["C:\\", "C:\\"], + ["C:\\abc", "C:\\"], + ["", ""], + + // unc + ["\\\\server\\share\\file_path", "\\\\server\\share\\"], + [ + "\\\\server two\\shared folder\\file path.zip", + "\\\\server two\\shared folder\\" + ], + ["\\\\teela\\admin$\\system32", "\\\\teela\\admin$\\"], + ["\\\\?\\UNC\\server\\share", "\\\\?\\UNC\\"] +]; + +const winSpecialCaseParseTests = [["/foo/bar", { root: "/" }]]; + +const winSpecialCaseFormatTests = [ + [{ dir: "some\\dir" }, "some\\dir\\"], + [{ base: "index.html" }, "index.html"], + [{ root: "C:\\" }, "C:\\"], + [{ name: "index", ext: ".html" }, "index.html"], + [{ dir: "some\\dir", name: "index", ext: ".html" }, "some\\dir\\index.html"], + [{ root: "C:\\", name: "index", ext: ".html" }, "C:\\index.html"], + [{}, ""] +]; + +const unixPaths = [ + // [path, root] + ["/home/user/dir/file.txt", "/"], + ["/home/user/a dir/another File.zip", "/"], + ["/home/user/a dir//another&File.", "/"], + ["/home/user/a$$$dir//another File.zip", "/"], + ["user/dir/another File.zip", ""], + ["file", ""], + [".\\file", ""], + ["./file", ""], + ["C:\\foo", ""], + ["/", "/"], + ["", ""], + [".", ""], + ["..", ""], + ["/foo", "/"], + ["/foo.", "/"], + ["/foo.bar", "/"], + ["/.", "/"], + ["/.foo", "/"], + ["/.foo.bar", "/"], + ["/foo/bar.baz", "/"] +]; + +const unixSpecialCaseFormatTests = [ + [{ dir: "some/dir" }, "some/dir/"], + [{ base: "index.html" }, "index.html"], + [{ root: "/" }, "/"], + [{ name: "index", ext: ".html" }, "index.html"], + [{ dir: "some/dir", name: "index", ext: ".html" }, "some/dir/index.html"], + [{ root: "/", name: "index", ext: ".html" }, "/index.html"], + [{}, ""] +]; + +function checkParseFormat(path: any, paths: any): void { + paths.forEach(function(p: Array<Record<string, unknown>>) { + const element = p[0]; + const output = path.parse(element); + assertEquals(typeof output.root, "string"); + assertEquals(typeof output.dir, "string"); + assertEquals(typeof output.base, "string"); + assertEquals(typeof output.ext, "string"); + assertEquals(typeof output.name, "string"); + assertEquals(path.format(output), element); + assertEquals(output.rooroot, undefined); + assertEquals(output.dir, output.dir ? path.dirname(element) : ""); + assertEquals(output.base, path.basename(element)); + }); +} + +function checkSpecialCaseParseFormat(path: any, testCases: any): void { + testCases.forEach(function(testCase: Array<Record<string, unknown>>) { + const element = testCase[0]; + const expect = testCase[1]; + const output = path.parse(element); + Object.keys(expect).forEach(function(key) { + assertEquals(output[key], expect[key]); + }); + }); +} + +function checkFormat(path: any, testCases: unknown[][]): void { + testCases.forEach(function(testCase) { + assertEquals(path.format(testCase[0]), testCase[1]); + }); +} + +test(function parseWin32() { + checkParseFormat(path.win32, winPaths); + checkSpecialCaseParseFormat(path.win32, winSpecialCaseParseTests); +}); + +test(function parse() { + checkParseFormat(path.posix, unixPaths); +}); + +test(function formatWin32() { + checkFormat(path.win32, winSpecialCaseFormatTests); +}); + +test(function format() { + checkFormat(path.posix, unixSpecialCaseFormatTests); +}); + +// Test removal of trailing path separators +const windowsTrailingTests = [ + [".\\", { root: "", dir: "", base: ".", ext: "", name: "." }], + ["\\\\", { root: "\\", dir: "\\", base: "", ext: "", name: "" }], + ["\\\\", { root: "\\", dir: "\\", base: "", ext: "", name: "" }], + [ + "c:\\foo\\\\\\", + { root: "c:\\", dir: "c:\\", base: "foo", ext: "", name: "foo" } + ], + [ + "D:\\foo\\\\\\bar.baz", + { + root: "D:\\", + dir: "D:\\foo\\\\", + base: "bar.baz", + ext: ".baz", + name: "bar" + } + ] +]; + +const posixTrailingTests = [ + ["./", { root: "", dir: "", base: ".", ext: "", name: "." }], + ["//", { root: "/", dir: "/", base: "", ext: "", name: "" }], + ["///", { root: "/", dir: "/", base: "", ext: "", name: "" }], + ["/foo///", { root: "/", dir: "/", base: "foo", ext: "", name: "foo" }], + [ + "/foo///bar.baz", + { root: "/", dir: "/foo//", base: "bar.baz", ext: ".baz", name: "bar" } + ] +]; + +test(function parseTrailingWin32() { + windowsTrailingTests.forEach(function(p) { + const actual = path.win32.parse(p[0] as string); + const expected = p[1]; + assertEquals(actual, expected); + }); +}); + +test(function parseTrailing() { + posixTrailingTests.forEach(function(p) { + const actual = path.posix.parse(p[0] as string); + const expected = p[1]; + assertEquals(actual, expected); + }); +}); diff --git a/std/fs/path/posix.ts b/std/fs/path/posix.ts new file mode 100644 index 000000000..4377fd542 --- /dev/null +++ b/std/fs/path/posix.ts @@ -0,0 +1,422 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd } = Deno; +import { FormatInputPathObject, ParsedPath } from "./interface.ts"; +import { CHAR_DOT, CHAR_FORWARD_SLASH } from "./constants.ts"; + +import { + assertPath, + normalizeString, + isPosixPathSeparator, + _format +} from "./utils.ts"; + +export const sep = "/"; +export const delimiter = ":"; + +// path.resolve([from ...], to) +export function resolve(...pathSegments: string[]): string { + let resolvedPath = ""; + let resolvedAbsolute = false; + + for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + let path: string; + + if (i >= 0) path = pathSegments[i]; + else path = cwd(); + + assertPath(path); + + // Skip empty entries + if (path.length === 0) { + continue; + } + + resolvedPath = `${path}/${resolvedPath}`; + resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeString( + resolvedPath, + !resolvedAbsolute, + "/", + isPosixPathSeparator + ); + + if (resolvedAbsolute) { + if (resolvedPath.length > 0) return `/${resolvedPath}`; + else return "/"; + } else if (resolvedPath.length > 0) return resolvedPath; + else return "."; +} + +export function normalize(path: string): string { + assertPath(path); + + if (path.length === 0) return "."; + + const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + const trailingSeparator = + path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH; + + // Normalize the path + path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator); + + if (path.length === 0 && !isAbsolute) path = "."; + if (path.length > 0 && trailingSeparator) path += "/"; + + if (isAbsolute) return `/${path}`; + return path; +} + +export function isAbsolute(path: string): boolean { + assertPath(path); + return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH; +} + +export function join(...paths: string[]): string { + if (paths.length === 0) return "."; + let joined: string | undefined; + for (let i = 0, len = paths.length; i < len; ++i) { + const path = paths[i]; + assertPath(path); + if (path.length > 0) { + if (!joined) joined = path; + else joined += `/${path}`; + } + } + if (!joined) return "."; + return normalize(joined); +} + +export function relative(from: string, to: string): string { + assertPath(from); + assertPath(to); + + if (from === to) return ""; + + from = resolve(from); + to = resolve(to); + + if (from === to) return ""; + + // Trim any leading backslashes + let fromStart = 1; + const fromEnd = from.length; + for (; fromStart < fromEnd; ++fromStart) { + if (from.charCodeAt(fromStart) !== CHAR_FORWARD_SLASH) break; + } + const fromLen = fromEnd - fromStart; + + // Trim any leading backslashes + let toStart = 1; + const toEnd = to.length; + for (; toStart < toEnd; ++toStart) { + if (to.charCodeAt(toStart) !== CHAR_FORWARD_SLASH) break; + } + const toLen = toEnd - toStart; + + // Compare paths to find the longest common path from root + const length = fromLen < toLen ? fromLen : toLen; + let lastCommonSep = -1; + let i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return to.slice(toStart + i + 1); + } else if (i === 0) { + // We get here if `from` is the root + // For example: from='/'; to='/foo' + return to.slice(toStart + i); + } + } else if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) { + // We get here if `to` is the exact base path for `from`. + // For example: from='/foo/bar/baz'; to='/foo/bar' + lastCommonSep = i; + } else if (i === 0) { + // We get here if `to` is the root. + // For example: from='/foo'; to='/' + lastCommonSep = 0; + } + } + break; + } + const fromCode = from.charCodeAt(fromStart + i); + const toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) break; + else if (fromCode === CHAR_FORWARD_SLASH) lastCommonSep = i; + } + + let out = ""; + // Generate the relative path based on the path difference between `to` + // and `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) { + if (out.length === 0) out += ".."; + else out += "/.."; + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) return out + to.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (to.charCodeAt(toStart) === CHAR_FORWARD_SLASH) ++toStart; + return to.slice(toStart); + } +} + +export function toNamespacedPath(path: string): string { + // Non-op on posix systems + return path; +} + +export function dirname(path: string): string { + assertPath(path); + if (path.length === 0) return "."; + const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + let end = -1; + let matchedSlash = true; + for (let i = path.length - 1; i >= 1; --i) { + if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } + + if (end === -1) return hasRoot ? "/" : "."; + if (hasRoot && end === 1) return "//"; + return path.slice(0, end); +} + +export function basename(path: string, ext = ""): string { + if (ext !== undefined && typeof ext !== "string") + throw new TypeError('"ext" argument must be a string'); + assertPath(path); + + let start = 0; + let end = -1; + let matchedSlash = true; + let i: number; + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) return ""; + let extIdx = ext.length - 1; + let firstNonSlashEnd = -1; + for (i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) end = firstNonSlashEnd; + else if (end === -1) end = path.length; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) return ""; + return path.slice(start, end); + } +} + +export function extname(path: string): string { + assertPath(path); + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ""; + } + return path.slice(startDot, end); +} + +export function format(pathObject: FormatInputPathObject): string { + /* eslint-disable max-len */ + if (pathObject === null || typeof pathObject !== "object") { + throw new TypeError( + `The "pathObject" argument must be of type Object. Received type ${typeof pathObject}` + ); + } + return _format("/", pathObject); +} + +export function parse(path: string): ParsedPath { + assertPath(path); + + const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" }; + if (path.length === 0) return ret; + const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + let start: number; + if (isAbsolute) { + ret.root = "/"; + start = 1; + } else { + start = 0; + } + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + let i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + + // Get non-dir info + for (; i >= start; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) { + ret.base = ret.name = path.slice(1, end); + } else { + ret.base = ret.name = path.slice(startPart, end); + } + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(1, startDot); + ret.base = path.slice(1, end); + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + } + ret.ext = path.slice(startDot, end); + } + + if (startPart > 0) ret.dir = path.slice(0, startPart - 1); + else if (isAbsolute) ret.dir = "/"; + + return ret; +} diff --git a/std/fs/path/relative_test.ts b/std/fs/path/relative_test.ts new file mode 100644 index 000000000..0188b5368 --- /dev/null +++ b/std/fs/path/relative_test.ts @@ -0,0 +1,73 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const relativeTests = { + win32: + // arguments result + [ + ["c:/blah\\blah", "d:/games", "d:\\games"], + ["c:/aaaa/bbbb", "c:/aaaa", ".."], + ["c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"], + ["c:/aaaa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"], + ["c:/aaaa/", "c:/aaaa/cccc", "cccc"], + ["c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"], + ["c:/aaaa/bbbb", "d:\\", "d:\\"], + ["c:/AaAa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"], + ["C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."], + [ + "C:\\foo\\test", + "C:\\foo\\test\\bar\\package.json", + "bar\\package.json" + ], + ["C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"], + ["C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."], + ["\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["C:\\baz-quux", "C:\\baz", "..\\baz"], + ["C:\\baz", "C:\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"], + ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], + ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], + ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"] + ], + posix: + // arguments result + [ + ["/var/lib", "/var", ".."], + ["/var/lib", "/bin", "../../bin"], + ["/var/lib", "/var/lib", ""], + ["/var/lib", "/var/apache", "../apache"], + ["/var/", "/var/lib", "lib"], + ["/", "/var/lib", "var/lib"], + ["/foo/test", "/foo/test/bar/package.json", "bar/package.json"], + ["/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."], + ["/foo/bar/baz-quux", "/foo/bar/baz", "../baz"], + ["/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"], + ["/baz-quux", "/baz", "../baz"], + ["/baz", "/baz-quux", "../baz-quux"] + ] +}; + +test(function relative() { + relativeTests.posix.forEach(function(p) { + const expected = p[2]; + const actual = path.posix.relative(p[0], p[1]); + assertEquals(actual, expected); + }); +}); + +test(function relativeWin32() { + relativeTests.win32.forEach(function(p) { + const expected = p[2]; + const actual = path.win32.relative(p[0], p[1]); + assertEquals(actual, expected); + }); +}); diff --git a/std/fs/path/resolve_test.ts b/std/fs/path/resolve_test.ts new file mode 100644 index 000000000..606570aad --- /dev/null +++ b/std/fs/path/resolve_test.ts @@ -0,0 +1,52 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd } = Deno; +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const windowsTests = + // arguments result + [ + [["c:/blah\\blah", "d:/games", "c:../a"], "c:\\blah\\a"], + [["c:/ignore", "d:\\a/b\\c/d", "\\e.exe"], "d:\\e.exe"], + [["c:/ignore", "c:/some/file"], "c:\\some\\file"], + [["d:/ignore", "d:some/dir//"], "d:\\ignore\\some\\dir"], + [["//server/share", "..", "relative\\"], "\\\\server\\share\\relative"], + [["c:/", "//"], "c:\\"], + [["c:/", "//dir"], "c:\\dir"], + [["c:/", "//server/share"], "\\\\server\\share\\"], + [["c:/", "//server//share"], "\\\\server\\share\\"], + [["c:/", "///some//dir"], "c:\\some\\dir"], + [ + ["C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"], + "C:\\foo\\tmp.3\\cycles\\root.js" + ] + ]; +const posixTests = + // arguments result + [ + [["/var/lib", "../", "file/"], "/var/file"], + [["/var/lib", "/../", "file/"], "/file"], + [["a/b/c/", "../../.."], cwd()], + [["."], cwd()], + [["/some/dir", ".", "/absolute/"], "/absolute"], + [["/foo/tmp.3/", "../tmp.3/cycles/root.js"], "/foo/tmp.3/cycles/root.js"] + ]; + +test(function resolve() { + posixTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.posix.resolve.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); + +test(function resolveWin32() { + windowsTests.forEach(function(p) { + const _p = p[0] as string[]; + const actual = path.win32.resolve.apply(null, _p); + assertEquals(actual, p[1]); + }); +}); diff --git a/std/fs/path/test.ts b/std/fs/path/test.ts new file mode 100644 index 000000000..3664ae5f1 --- /dev/null +++ b/std/fs/path/test.ts @@ -0,0 +1,9 @@ +import "./basename_test.ts"; +import "./dirname_test.ts"; +import "./extname_test.ts"; +import "./isabsolute_test.ts"; +import "./join_test.ts"; +import "./parse_format_test.ts"; +import "./relative_test.ts"; +import "./resolve_test.ts"; +import "./zero_length_strings_test.ts"; diff --git a/std/fs/path/utils.ts b/std/fs/path/utils.ts new file mode 100644 index 000000000..7a4cd7299 --- /dev/null +++ b/std/fs/path/utils.ts @@ -0,0 +1,116 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +import { FormatInputPathObject } from "./interface.ts"; +import { + CHAR_UPPERCASE_A, + CHAR_LOWERCASE_A, + CHAR_UPPERCASE_Z, + CHAR_LOWERCASE_Z, + CHAR_DOT, + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH +} from "./constants.ts"; + +export function assertPath(path: string): void { + if (typeof path !== "string") { + throw new TypeError( + `Path must be a string. Received ${JSON.stringify(path)}` + ); + } +} + +export function isPosixPathSeparator(code: number): boolean { + return code === CHAR_FORWARD_SLASH; +} + +export function isPathSeparator(code: number): boolean { + return isPosixPathSeparator(code) || code === CHAR_BACKWARD_SLASH; +} + +export function isWindowsDeviceRoot(code: number): boolean { + return ( + (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) || + (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) + ); +} + +// Resolves . and .. elements in a path with directory names +export function normalizeString( + path: string, + allowAboveRoot: boolean, + separator: string, + isPathSeparator: (code: number) => boolean +): string { + let res = ""; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let code: number; + for (let i = 0, len = path.length; i <= len; ++i) { + if (i < len) code = path.charCodeAt(i); + else if (isPathSeparator(code!)) break; + else code = CHAR_FORWARD_SLASH; + + if (isPathSeparator(code)) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if ( + res.length < 2 || + lastSegmentLength !== 2 || + res.charCodeAt(res.length - 1) !== CHAR_DOT || + res.charCodeAt(res.length - 2) !== CHAR_DOT + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf(separator); + if (lastSlashIndex === -1) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (res.length === 2 || res.length === 1) { + res = ""; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) res += `${separator}..`; + else res = ".."; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); + else res = path.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === CHAR_DOT && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} + +export function _format( + sep: string, + pathObject: FormatInputPathObject +): string { + const dir: string | undefined = pathObject.dir || pathObject.root; + const base: string = + pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); + if (!dir) return base; + if (dir === pathObject.root) return dir + base; + return dir + sep + base; +} diff --git a/std/fs/path/win32.ts b/std/fs/path/win32.ts new file mode 100644 index 000000000..79e04ea6e --- /dev/null +++ b/std/fs/path/win32.ts @@ -0,0 +1,896 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd, env } = Deno; +import { FormatInputPathObject, ParsedPath } from "./interface.ts"; +import { + CHAR_DOT, + CHAR_BACKWARD_SLASH, + CHAR_COLON, + CHAR_QUESTION_MARK +} from "./constants.ts"; + +import { + assertPath, + isPathSeparator, + isWindowsDeviceRoot, + normalizeString, + _format +} from "./utils.ts"; + +export const sep = "\\"; +export const delimiter = ";"; + +export function resolve(...pathSegments: string[]): string { + let resolvedDevice = ""; + let resolvedTail = ""; + let resolvedAbsolute = false; + + for (let i = pathSegments.length - 1; i >= -1; i--) { + let path: string; + if (i >= 0) { + path = pathSegments[i]; + } else if (!resolvedDevice) { + path = cwd(); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive, or the process cwd if + // the drive cwd is not available. We're sure the device is not + // a UNC path at this points, because UNC paths are always absolute. + path = env()[`=${resolvedDevice}`] || cwd(); + + // Verify that a cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if ( + path === undefined || + path.slice(0, 3).toLowerCase() !== `${resolvedDevice.toLowerCase()}\\` + ) { + path = `${resolvedDevice}\\`; + } + } + + assertPath(path); + + const len = path.length; + + // Skip empty entries + if (len === 0) continue; + + let rootEnd = 0; + let device = ""; + let isAbsolute = false; + const code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an + // absolute path of some kind (UNC or otherwise) + isAbsolute = true; + + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + const firstPart = path.slice(last, j); + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + device = `\\\\${firstPart}\\${path.slice(last)}`; + rootEnd = j; + } else if (j !== last) { + // We matched a UNC root with leftovers + + device = `\\\\${firstPart}\\${path.slice(last, j)}`; + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + device = path.slice(0, 2); + rootEnd = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator + rootEnd = 1; + isAbsolute = true; + } + + if ( + device.length > 0 && + resolvedDevice.length > 0 && + device.toLowerCase() !== resolvedDevice.toLowerCase() + ) { + // This path points to another device so it is not applicable + continue; + } + + if (resolvedDevice.length === 0 && device.length > 0) { + resolvedDevice = device; + } + if (!resolvedAbsolute) { + resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`; + resolvedAbsolute = isAbsolute; + } + + if (resolvedAbsolute && resolvedDevice.length > 0) break; + } + + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) + + // Normalize the tail path + resolvedTail = normalizeString( + resolvedTail, + !resolvedAbsolute, + "\\", + isPathSeparator + ); + + return resolvedDevice + (resolvedAbsolute ? "\\" : "") + resolvedTail || "."; +} + +export function normalize(path: string): string { + assertPath(path); + const len = path.length; + if (len === 0) return "."; + let rootEnd = 0; + let device: string | undefined; + let isAbsolute = false; + const code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an absolute + // path of some kind (UNC or otherwise) + isAbsolute = true; + + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + const firstPart = path.slice(last, j); + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + // Return the normalized version of the UNC root since there + // is nothing left to process + + return `\\\\${firstPart}\\${path.slice(last)}\\`; + } else if (j !== last) { + // We matched a UNC root with leftovers + + device = `\\\\${firstPart}\\${path.slice(last, j)}`; + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + device = path.slice(0, 2); + rootEnd = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator, exit early to avoid unnecessary + // work + return "\\"; + } + + let tail: string; + if (rootEnd < len) { + tail = normalizeString( + path.slice(rootEnd), + !isAbsolute, + "\\", + isPathSeparator + ); + } else { + tail = ""; + } + if (tail.length === 0 && !isAbsolute) tail = "."; + if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) + tail += "\\"; + if (device === undefined) { + if (isAbsolute) { + if (tail.length > 0) return `\\${tail}`; + else return "\\"; + } else if (tail.length > 0) { + return tail; + } else { + return ""; + } + } else if (isAbsolute) { + if (tail.length > 0) return `${device}\\${tail}`; + else return `${device}\\`; + } else if (tail.length > 0) { + return device + tail; + } else { + return device; + } +} + +export function isAbsolute(path: string): boolean { + assertPath(path); + const len = path.length; + if (len === 0) return false; + + const code = path.charCodeAt(0); + if (isPathSeparator(code)) { + return true; + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (len > 2 && path.charCodeAt(1) === CHAR_COLON) { + if (isPathSeparator(path.charCodeAt(2))) return true; + } + } + return false; +} + +export function join(...paths: string[]): string { + const pathsCount = paths.length; + if (pathsCount === 0) return "."; + + let joined: string | undefined; + let firstPart: string; + for (let i = 0; i < pathsCount; ++i) { + const path = paths[i]; + assertPath(path); + if (path.length > 0) { + if (joined === undefined) joined = firstPart = path; + else joined += `\\${path}`; + } + } + + if (joined === undefined) return "."; + + // Make sure that the joined path doesn't start with two slashes, because + // normalize() will mistake it for an UNC path then. + // + // This step is skipped when it is very clear that the user actually + // intended to point at an UNC path. This is assumed when the first + // non-empty string arguments starts with exactly two slashes followed by + // at least one more non-slash character. + // + // Note that for normalize() to treat a path as an UNC path it needs to + // have at least 2 components, so we don't filter for that here. + // This means that the user can use join to construct UNC paths from + // a server name and a share name; for example: + // path.join('//server', 'share') -> '\\\\server\\share\\') + let needsReplace = true; + let slashCount = 0; + firstPart = firstPart!; + if (isPathSeparator(firstPart.charCodeAt(0))) { + ++slashCount; + const firstLen = firstPart.length; + if (firstLen > 1) { + if (isPathSeparator(firstPart.charCodeAt(1))) { + ++slashCount; + if (firstLen > 2) { + if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount; + else { + // We matched a UNC path in the first part + needsReplace = false; + } + } + } + } + } + if (needsReplace) { + // Find any more consecutive slashes we need to replace + for (; slashCount < joined.length; ++slashCount) { + if (!isPathSeparator(joined.charCodeAt(slashCount))) break; + } + + // Replace the slashes if needed + if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`; + } + + return normalize(joined); +} + +// It will solve the relative path from `from` to `to`, for instance: +// from = 'C:\\orandea\\test\\aaa' +// to = 'C:\\orandea\\impl\\bbb' +// The output of the function should be: '..\\..\\impl\\bbb' +export function relative(from: string, to: string): string { + assertPath(from); + assertPath(to); + + if (from === to) return ""; + + const fromOrig = resolve(from); + const toOrig = resolve(to); + + if (fromOrig === toOrig) return ""; + + from = fromOrig.toLowerCase(); + to = toOrig.toLowerCase(); + + if (from === to) return ""; + + // Trim any leading backslashes + let fromStart = 0; + let fromEnd = from.length; + for (; fromStart < fromEnd; ++fromStart) { + if (from.charCodeAt(fromStart) !== CHAR_BACKWARD_SLASH) break; + } + // Trim trailing backslashes (applicable to UNC paths only) + for (; fromEnd - 1 > fromStart; --fromEnd) { + if (from.charCodeAt(fromEnd - 1) !== CHAR_BACKWARD_SLASH) break; + } + const fromLen = fromEnd - fromStart; + + // Trim any leading backslashes + let toStart = 0; + let toEnd = to.length; + for (; toStart < toEnd; ++toStart) { + if (to.charCodeAt(toStart) !== CHAR_BACKWARD_SLASH) break; + } + // Trim trailing backslashes (applicable to UNC paths only) + for (; toEnd - 1 > toStart; --toEnd) { + if (to.charCodeAt(toEnd - 1) !== CHAR_BACKWARD_SLASH) break; + } + const toLen = toEnd - toStart; + + // Compare paths to find the longest common path from root + const length = fromLen < toLen ? fromLen : toLen; + let lastCommonSep = -1; + let i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) { + // We get here if `from` is the exact base path for `to`. + // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz' + return toOrig.slice(toStart + i + 1); + } else if (i === 2) { + // We get here if `from` is the device root. + // For example: from='C:\\'; to='C:\\foo' + return toOrig.slice(toStart + i); + } + } + if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) { + // We get here if `to` is the exact base path for `from`. + // For example: from='C:\\foo\\bar'; to='C:\\foo' + lastCommonSep = i; + } else if (i === 2) { + // We get here if `to` is the device root. + // For example: from='C:\\foo\\bar'; to='C:\\' + lastCommonSep = 3; + } + } + break; + } + const fromCode = from.charCodeAt(fromStart + i); + const toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) break; + else if (fromCode === CHAR_BACKWARD_SLASH) lastCommonSep = i; + } + + // We found a mismatch before the first common path separator was seen, so + // return the original `to`. + if (i !== length && lastCommonSep === -1) { + return toOrig; + } + + let out = ""; + if (lastCommonSep === -1) lastCommonSep = 0; + // Generate the relative path based on the path difference between `to` and + // `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) { + if (out.length === 0) out += ".."; + else out += "\\.."; + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) return out + toOrig.slice(toStart + lastCommonSep, toEnd); + else { + toStart += lastCommonSep; + if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) ++toStart; + return toOrig.slice(toStart, toEnd); + } +} + +export function toNamespacedPath(path: string): string { + // Note: this will *probably* throw somewhere. + if (typeof path !== "string") return path; + if (path.length === 0) return ""; + + const resolvedPath = resolve(path); + + if (resolvedPath.length >= 3) { + if (resolvedPath.charCodeAt(0) === CHAR_BACKWARD_SLASH) { + // Possible UNC root + + if (resolvedPath.charCodeAt(1) === CHAR_BACKWARD_SLASH) { + const code = resolvedPath.charCodeAt(2); + if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) { + // Matched non-long UNC root, convert the path to a long UNC path + return `\\\\?\\UNC\\${resolvedPath.slice(2)}`; + } + } + } else if (isWindowsDeviceRoot(resolvedPath.charCodeAt(0))) { + // Possible device root + + if ( + resolvedPath.charCodeAt(1) === CHAR_COLON && + resolvedPath.charCodeAt(2) === CHAR_BACKWARD_SLASH + ) { + // Matched device root, convert the path to a long UNC path + return `\\\\?\\${resolvedPath}`; + } + } + } + + return path; +} + +export function dirname(path: string): string { + assertPath(path); + const len = path.length; + if (len === 0) return "."; + let rootEnd = -1; + let end = -1; + let matchedSlash = true; + let offset = 0; + const code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + rootEnd = offset = 1; + + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + return path; + } + if (j !== last) { + // We matched a UNC root with leftovers + + // Offset by 1 to include the separator after the UNC root to + // treat it as a "normal root" on top of a (UNC) root + rootEnd = offset = j + 1; + } + } + } + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + rootEnd = offset = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) rootEnd = offset = 3; + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work + return path; + } + + for (let i = len - 1; i >= offset; --i) { + if (isPathSeparator(path.charCodeAt(i))) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } + + if (end === -1) { + if (rootEnd === -1) return "."; + else end = rootEnd; + } + return path.slice(0, end); +} + +export function basename(path: string, ext = ""): string { + if (ext !== undefined && typeof ext !== "string") + throw new TypeError('"ext" argument must be a string'); + + assertPath(path); + + let start = 0; + let end = -1; + let matchedSlash = true; + let i: number; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + if (path.length >= 2) { + const drive = path.charCodeAt(0); + if (isWindowsDeviceRoot(drive)) { + if (path.charCodeAt(1) === CHAR_COLON) start = 2; + } + } + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) return ""; + let extIdx = ext.length - 1; + let firstNonSlashEnd = -1; + for (i = path.length - 1; i >= start; --i) { + const code = path.charCodeAt(i); + if (isPathSeparator(code)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) end = firstNonSlashEnd; + else if (end === -1) end = path.length; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= start; --i) { + if (isPathSeparator(path.charCodeAt(i))) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) return ""; + return path.slice(start, end); + } +} + +export function extname(path: string): string { + assertPath(path); + let start = 0; + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + + if ( + path.length >= 2 && + path.charCodeAt(1) === CHAR_COLON && + isWindowsDeviceRoot(path.charCodeAt(0)) + ) { + start = startPart = 2; + } + + for (let i = path.length - 1; i >= start; --i) { + const code = path.charCodeAt(i); + if (isPathSeparator(code)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ""; + } + return path.slice(startDot, end); +} + +export function format(pathObject: FormatInputPathObject): string { + /* eslint-disable max-len */ + if (pathObject === null || typeof pathObject !== "object") { + throw new TypeError( + `The "pathObject" argument must be of type Object. Received type ${typeof pathObject}` + ); + } + return _format("\\", pathObject); +} + +export function parse(path: string): ParsedPath { + assertPath(path); + + const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" }; + + const len = path.length; + if (len === 0) return ret; + + let rootEnd = 0; + let code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (isPathSeparator(code)) { + // Possible UNC root + + rootEnd = 1; + if (isPathSeparator(path.charCodeAt(1))) { + // Matched double path separator at beginning + let j = 2; + let last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + if (!isPathSeparator(path.charCodeAt(j))) break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + if (isPathSeparator(path.charCodeAt(j))) break; + } + if (j === len) { + // We matched a UNC root only + + rootEnd = j; + } else if (j !== last) { + // We matched a UNC root with leftovers + + rootEnd = j + 1; + } + } + } + } + } else if (isWindowsDeviceRoot(code)) { + // Possible device root + + if (path.charCodeAt(1) === CHAR_COLON) { + rootEnd = 2; + if (len > 2) { + if (isPathSeparator(path.charCodeAt(2))) { + if (len === 3) { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + ret.root = ret.dir = path; + return ret; + } + rootEnd = 3; + } + } else { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + ret.root = ret.dir = path; + return ret; + } + } + } + } else if (isPathSeparator(code)) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work + ret.root = ret.dir = path; + return ret; + } + + if (rootEnd > 0) ret.root = path.slice(0, rootEnd); + + let startDot = -1; + let startPart = rootEnd; + let end = -1; + let matchedSlash = true; + let i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + + // Get non-dir info + for (; i >= rootEnd; --i) { + code = path.charCodeAt(i); + if (isPathSeparator(code)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + if (end !== -1) { + ret.base = ret.name = path.slice(startPart, end); + } + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + ret.ext = path.slice(startDot, end); + } + + // If the directory is the root, use the entire root as the `dir` including + // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the + // trailing slash (`C:\abc\def` -> `C:\abc`). + if (startPart > 0 && startPart !== rootEnd) { + ret.dir = path.slice(0, startPart - 1); + } else ret.dir = ret.root; + + return ret; +} diff --git a/std/fs/path/zero_length_strings_test.ts b/std/fs/path/zero_length_strings_test.ts new file mode 100644 index 000000000..744e97735 --- /dev/null +++ b/std/fs/path/zero_length_strings_test.ts @@ -0,0 +1,49 @@ +// Copyright the Browserify authors. MIT License. +// Ported from https://github.com/browserify/path-browserify/ + +const { cwd } = Deno; +import { test } from "../../testing/mod.ts"; +import { assertEquals } from "../../testing/asserts.ts"; +import * as path from "./mod.ts"; + +const pwd = cwd(); + +test(function joinZeroLength() { + // join will internally ignore all the zero-length strings and it will return + // '.' if the joined string is a zero-length string. + assertEquals(path.posix.join(""), "."); + assertEquals(path.posix.join("", ""), "."); + if (path.win32) assertEquals(path.win32.join(""), "."); + if (path.win32) assertEquals(path.win32.join("", ""), "."); + assertEquals(path.join(pwd), pwd); + assertEquals(path.join(pwd, ""), pwd); +}); + +test(function normalizeZeroLength() { + // normalize will return '.' if the input is a zero-length string + assertEquals(path.posix.normalize(""), "."); + if (path.win32) assertEquals(path.win32.normalize(""), "."); + assertEquals(path.normalize(pwd), pwd); +}); + +test(function isAbsoluteZeroLength() { + // Since '' is not a valid path in any of the common environments, + // return false + assertEquals(path.posix.isAbsolute(""), false); + if (path.win32) assertEquals(path.win32.isAbsolute(""), false); +}); + +test(function resolveZeroLength() { + // resolve, internally ignores all the zero-length strings and returns the + // current working directory + assertEquals(path.resolve(""), pwd); + assertEquals(path.resolve("", ""), pwd); +}); + +test(function relativeZeroLength() { + // relative, internally calls resolve. So, '' is actually the current + // directory + assertEquals(path.relative("", pwd), ""); + assertEquals(path.relative(pwd, ""), ""); + assertEquals(path.relative(pwd, pwd), ""); +}); diff --git a/std/fs/read_file_str.ts b/std/fs/read_file_str.ts new file mode 100644 index 000000000..9f87c9338 --- /dev/null +++ b/std/fs/read_file_str.ts @@ -0,0 +1,33 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +export interface ReadOptions { + encoding?: string; +} + +/** + * Read file synchronously and output it as a string. + * + * @param filename File to read + * @param opts Read options + */ +export function readFileStrSync( + filename: string, + opts: ReadOptions = {} +): string { + const decoder = new TextDecoder(opts.encoding); + return decoder.decode(Deno.readFileSync(filename)); +} + +/** + * Read file and output it as a string. + * + * @param filename File to read + * @param opts Read options + */ +export async function readFileStr( + filename: string, + opts: ReadOptions = {} +): Promise<string> { + const decoder = new TextDecoder(opts.encoding); + return decoder.decode(await Deno.readFile(filename)); +} diff --git a/std/fs/read_file_str_test.ts b/std/fs/read_file_str_test.ts new file mode 100644 index 000000000..d7d67d8d4 --- /dev/null +++ b/std/fs/read_file_str_test.ts @@ -0,0 +1,20 @@ +import { test } from "../testing/mod.ts"; +import { assert } from "../testing/asserts.ts"; +import { readFileStrSync, readFileStr } from "./read_file_str.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(function testReadFileSync(): void { + const jsonFile = path.join(testdataDir, "json_valid_obj.json"); + const strFile = readFileStrSync(jsonFile); + assert(typeof strFile === "string"); + assert(strFile.length > 0); +}); + +test(async function testReadFile(): Promise<void> { + const jsonFile = path.join(testdataDir, "json_valid_obj.json"); + const strFile = await readFileStr(jsonFile); + assert(typeof strFile === "string"); + assert(strFile.length > 0); +}); diff --git a/std/fs/read_json.ts b/std/fs/read_json.ts new file mode 100644 index 000000000..ca5928afe --- /dev/null +++ b/std/fs/read_json.ts @@ -0,0 +1,29 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +/** Reads a JSON file and then parses it into an object */ +export async function readJson(filePath: string): Promise<unknown> { + const decoder = new TextDecoder("utf-8"); + + const content = decoder.decode(await Deno.readFile(filePath)); + + try { + return JSON.parse(content); + } catch (err) { + err.message = `${filePath}: ${err.message}`; + throw err; + } +} + +/** Reads a JSON file and then parses it into an object */ +export function readJsonSync(filePath: string): unknown { + const decoder = new TextDecoder("utf-8"); + + const content = decoder.decode(Deno.readFileSync(filePath)); + + try { + return JSON.parse(content); + } catch (err) { + err.message = `${filePath}: ${err.message}`; + throw err; + } +} diff --git a/std/fs/read_json_test.ts b/std/fs/read_json_test.ts new file mode 100644 index 000000000..28f733055 --- /dev/null +++ b/std/fs/read_json_test.ts @@ -0,0 +1,109 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrowsAsync, + assertThrows +} from "../testing/asserts.ts"; +import { readJson, readJsonSync } from "./read_json.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function readJsonFileNotExists(): Promise<void> { + const emptyJsonFile = path.join(testdataDir, "json_not_exists.json"); + + await assertThrowsAsync( + async (): Promise<void> => { + await readJson(emptyJsonFile); + } + ); +}); + +test(async function readEmptyJsonFile(): Promise<void> { + const emptyJsonFile = path.join(testdataDir, "json_empty.json"); + + await assertThrowsAsync( + async (): Promise<void> => { + await readJson(emptyJsonFile); + } + ); +}); + +test(async function readInvalidJsonFile(): Promise<void> { + const invalidJsonFile = path.join(testdataDir, "json_invalid.json"); + + await assertThrowsAsync( + async (): Promise<void> => { + await readJson(invalidJsonFile); + } + ); +}); + +test(async function readValidArrayJsonFile(): Promise<void> { + const invalidJsonFile = path.join(testdataDir, "json_valid_array.json"); + + const json = await readJson(invalidJsonFile); + + assertEquals(json, ["1", "2", "3"]); +}); + +test(async function readValidObjJsonFile(): Promise<void> { + const invalidJsonFile = path.join(testdataDir, "json_valid_obj.json"); + + const json = await readJson(invalidJsonFile); + + assertEquals(json, { key1: "value1", key2: "value2" }); +}); + +test(async function readValidObjJsonFileWithRelativePath(): Promise<void> { + const json = await readJson("./fs/testdata/json_valid_obj.json"); + + assertEquals(json, { key1: "value1", key2: "value2" }); +}); + +test(function readJsonFileNotExistsSync(): void { + const emptyJsonFile = path.join(testdataDir, "json_not_exists.json"); + + assertThrows((): void => { + readJsonSync(emptyJsonFile); + }); +}); + +test(function readEmptyJsonFileSync(): void { + const emptyJsonFile = path.join(testdataDir, "json_empty.json"); + + assertThrows((): void => { + readJsonSync(emptyJsonFile); + }); +}); + +test(function readInvalidJsonFile(): void { + const invalidJsonFile = path.join(testdataDir, "json_invalid.json"); + + assertThrows((): void => { + readJsonSync(invalidJsonFile); + }); +}); + +test(function readValidArrayJsonFileSync(): void { + const invalidJsonFile = path.join(testdataDir, "json_valid_array.json"); + + const json = readJsonSync(invalidJsonFile); + + assertEquals(json, ["1", "2", "3"]); +}); + +test(function readValidObjJsonFileSync(): void { + const invalidJsonFile = path.join(testdataDir, "json_valid_obj.json"); + + const json = readJsonSync(invalidJsonFile); + + assertEquals(json, { key1: "value1", key2: "value2" }); +}); + +test(function readValidObjJsonFileSyncWithRelativePath(): void { + const json = readJsonSync("./fs/testdata/json_valid_obj.json"); + + assertEquals(json, { key1: "value1", key2: "value2" }); +}); diff --git a/std/fs/testdata/0-link.ts b/std/fs/testdata/0-link.ts new file mode 120000 index 000000000..24c6b8053 --- /dev/null +++ b/std/fs/testdata/0-link.ts @@ -0,0 +1 @@ +./fs/testdata/0.ts
\ No newline at end of file diff --git a/std/fs/testdata/0.ts b/std/fs/testdata/0.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/std/fs/testdata/0.ts diff --git a/std/fs/testdata/copy_dir/0.txt b/std/fs/testdata/copy_dir/0.txt new file mode 100644 index 000000000..f3a34851d --- /dev/null +++ b/std/fs/testdata/copy_dir/0.txt @@ -0,0 +1 @@ +text
\ No newline at end of file diff --git a/std/fs/testdata/copy_dir/nest/0.txt b/std/fs/testdata/copy_dir/nest/0.txt new file mode 100644 index 000000000..cd1b98cb3 --- /dev/null +++ b/std/fs/testdata/copy_dir/nest/0.txt @@ -0,0 +1 @@ +nest
\ No newline at end of file diff --git a/std/fs/testdata/copy_dir_link_file/0.txt b/std/fs/testdata/copy_dir_link_file/0.txt new file mode 120000 index 000000000..63413ea1c --- /dev/null +++ b/std/fs/testdata/copy_dir_link_file/0.txt @@ -0,0 +1 @@ +./fs/testdata/copy_dir/0.txt
\ No newline at end of file diff --git a/std/fs/testdata/copy_file.txt b/std/fs/testdata/copy_file.txt new file mode 100644 index 000000000..84c22fd8a --- /dev/null +++ b/std/fs/testdata/copy_file.txt @@ -0,0 +1 @@ +txt
\ No newline at end of file diff --git a/std/fs/testdata/glob/abc b/std/fs/testdata/glob/abc new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/std/fs/testdata/glob/abc diff --git a/std/fs/testdata/glob/abcdef b/std/fs/testdata/glob/abcdef new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/std/fs/testdata/glob/abcdef diff --git a/std/fs/testdata/glob/abcdefghi b/std/fs/testdata/glob/abcdefghi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/std/fs/testdata/glob/abcdefghi diff --git a/std/fs/testdata/glob/subdir/abc b/std/fs/testdata/glob/subdir/abc new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/std/fs/testdata/glob/subdir/abc diff --git a/std/fs/testdata/json_empty.json b/std/fs/testdata/json_empty.json new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/std/fs/testdata/json_empty.json diff --git a/std/fs/testdata/json_invalid.json b/std/fs/testdata/json_invalid.json new file mode 100644 index 000000000..dd9f98ff0 --- /dev/null +++ b/std/fs/testdata/json_invalid.json @@ -0,0 +1,5 @@ +{ + [ + "here is a invalid json file" + ] +}
\ No newline at end of file diff --git a/std/fs/testdata/json_valid_array.json b/std/fs/testdata/json_valid_array.json new file mode 100644 index 000000000..904968e15 --- /dev/null +++ b/std/fs/testdata/json_valid_array.json @@ -0,0 +1,5 @@ +[ + "1", + "2", + "3" +]
\ No newline at end of file diff --git a/std/fs/testdata/json_valid_obj.json b/std/fs/testdata/json_valid_obj.json new file mode 100644 index 000000000..88b3d7123 --- /dev/null +++ b/std/fs/testdata/json_valid_obj.json @@ -0,0 +1,4 @@ +{ + "key1": "value1", + "key2": "value2" +}
\ No newline at end of file diff --git a/std/fs/utils.ts b/std/fs/utils.ts new file mode 100644 index 000000000..f644f4869 --- /dev/null +++ b/std/fs/utils.ts @@ -0,0 +1,45 @@ +import * as path from "./path/mod.ts"; + +/** + * Test whether or not `dest` is a sub-directory of `src` + * @param src src file path + * @param dest dest file path + * @param sep path separator + */ +export function isSubdir( + src: string, + dest: string, + sep: string = path.sep +): boolean { + if (src === dest) { + return false; + } + const srcArray = src.split(sep); + const destArray = dest.split(sep); + // see: https://github.com/Microsoft/TypeScript/issues/30821 + return srcArray.reduce( + // @ts-ignore + (acc: true, current: string, i: number): boolean => { + return acc && destArray[i] === current; + }, + true + ); +} + +export type PathType = "file" | "dir" | "symlink"; + +/** + * Get a human readable file type string. + * + * @param fileInfo A FileInfo describes a file and is returned by `stat`, + * `lstat` + */ +export function getFileInfoType(fileInfo: Deno.FileInfo): PathType | undefined { + return fileInfo.isFile() + ? "file" + : fileInfo.isDirectory() + ? "dir" + : fileInfo.isSymlink() + ? "symlink" + : undefined; +} diff --git a/std/fs/utils_test.ts b/std/fs/utils_test.ts new file mode 100644 index 000000000..5b33842ad --- /dev/null +++ b/std/fs/utils_test.ts @@ -0,0 +1,64 @@ +// Copyright the Browserify authors. MIT License. + +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { isSubdir, getFileInfoType, PathType } from "./utils.ts"; +import * as path from "./path/mod.ts"; +import { ensureFileSync } from "./ensure_file.ts"; +import { ensureDirSync } from "./ensure_dir.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(function _isSubdir(): void { + const pairs = [ + ["", "", false, path.posix.sep], + ["/first/second", "/first", false, path.posix.sep], + ["/first", "/first", false, path.posix.sep], + ["/first", "/first/second", true, path.posix.sep], + ["first", "first/second", true, path.posix.sep], + ["../first", "../first/second", true, path.posix.sep], + ["c:\\first", "c:\\first", false, path.win32.sep], + ["c:\\first", "c:\\first\\second", true, path.win32.sep] + ]; + + pairs.forEach(function(p): void { + const src = p[0] as string; + const dest = p[1] as string; + const expected = p[2] as boolean; + const sep = p[3] as string; + assertEquals( + isSubdir(src, dest, sep), + expected, + `'${src}' should ${expected ? "" : "not"} be parent dir of '${dest}'` + ); + }); +}); + +test(function _getFileInfoType(): void { + const pairs = [ + [path.join(testdataDir, "file_type_1"), "file"], + [path.join(testdataDir, "file_type_dir_1"), "dir"] + ]; + + pairs.forEach(function(p): void { + const filePath = p[0] as string; + const type = p[1] as PathType; + switch (type) { + case "file": + ensureFileSync(filePath); + break; + case "dir": + ensureDirSync(filePath); + break; + case "symlink": + // TODO(axetroy): test symlink + break; + } + + const stat = Deno.statSync(filePath); + + Deno.removeSync(filePath, { recursive: true }); + + assertEquals(getFileInfoType(stat), type); + }); +}); diff --git a/std/fs/walk.ts b/std/fs/walk.ts new file mode 100644 index 000000000..4808598af --- /dev/null +++ b/std/fs/walk.ts @@ -0,0 +1,176 @@ +// Documentation and interface for walk were adapted from Go +// https://golang.org/pkg/path/filepath/#Walk +// Copyright 2009 The Go Authors. All rights reserved. BSD license. +import { unimplemented } from "../testing/asserts.ts"; +import { join } from "./path/mod.ts"; +const { readDir, readDirSync, stat, statSync } = Deno; +type FileInfo = Deno.FileInfo; + +export interface WalkOptions { + maxDepth?: number; + includeFiles?: boolean; + includeDirs?: boolean; + followSymlinks?: boolean; + exts?: string[]; + match?: RegExp[]; + skip?: RegExp[]; + onError?: (err: Error) => void; +} + +function patternTest(patterns: RegExp[], path: string): boolean { + // Forced to reset last index on regex while iterating for have + // consistent results. + // See: https://stackoverflow.com/a/1520853 + return patterns.some((pattern): boolean => { + const r = pattern.test(path); + pattern.lastIndex = 0; + return r; + }); +} + +function include(filename: string, options: WalkOptions): boolean { + if ( + options.exts && + !options.exts.some((ext): boolean => filename.endsWith(ext)) + ) { + return false; + } + if (options.match && !patternTest(options.match, filename)) { + return false; + } + if (options.skip && patternTest(options.skip, filename)) { + return false; + } + return true; +} + +export interface WalkInfo { + filename: string; + info: FileInfo; +} + +/** Walks the file tree rooted at root, yielding each file or directory in the + * tree filtered according to the given options. The files are walked in lexical + * order, which makes the output deterministic but means that for very large + * directories walk() can be inefficient. + * + * Options: + * - maxDepth?: number = Infinity; + * - includeFiles?: boolean = true; + * - includeDirs?: boolean = true; + * - followSymlinks?: boolean = false; + * - exts?: string[]; + * - match?: RegExp[]; + * - skip?: RegExp[]; + * - onError?: (err: Error) => void; + * + * for await (const { filename, info } of walk(".")) { + * console.log(filename); + * assert(info.isFile()); + * }; + */ +export async function* walk( + root: string, + options: WalkOptions = {} +): AsyncIterableIterator<WalkInfo> { + const maxDepth = options.maxDepth != undefined ? options.maxDepth! : Infinity; + if (maxDepth < 0) { + return; + } + if (options.includeDirs != false && include(root, options)) { + let rootInfo: FileInfo; + try { + rootInfo = await stat(root); + } catch (err) { + if (options.onError) { + options.onError(err); + return; + } + } + yield { filename: root, info: rootInfo! }; + } + if (maxDepth < 1 || patternTest(options.skip || [], root)) { + return; + } + let ls: FileInfo[] = []; + try { + ls = await readDir(root); + } catch (err) { + if (options.onError) { + options.onError(err); + } + } + for (const info of ls) { + if (info.isSymlink()) { + if (options.followSymlinks) { + // TODO(ry) Re-enable followSymlinks. + unimplemented(); + } else { + continue; + } + } + + const filename = join(root, info.name!); + + if (info.isFile()) { + if (options.includeFiles != false && include(filename, options)) { + yield { filename, info }; + } + } else { + yield* walk(filename, { ...options, maxDepth: maxDepth - 1 }); + } + } +} + +/** Same as walk() but uses synchronous ops */ +export function* walkSync( + root: string, + options: WalkOptions = {} +): IterableIterator<WalkInfo> { + const maxDepth = options.maxDepth != undefined ? options.maxDepth! : Infinity; + if (maxDepth < 0) { + return; + } + if (options.includeDirs != false && include(root, options)) { + let rootInfo: FileInfo; + try { + rootInfo = statSync(root); + } catch (err) { + if (options.onError) { + options.onError(err); + return; + } + } + yield { filename: root, info: rootInfo! }; + } + if (maxDepth < 1 || patternTest(options.skip || [], root)) { + return; + } + let ls: FileInfo[] = []; + try { + ls = readDirSync(root); + } catch (err) { + if (options.onError) { + options.onError(err); + } + } + for (const info of ls) { + if (info.isSymlink()) { + if (options.followSymlinks) { + unimplemented(); + } else { + continue; + } + } + + const filename = join(root, info.name!); + + if (info.isFile()) { + if (options.includeFiles != false && include(filename, options)) { + yield { filename, info }; + } + } else { + yield* walkSync(filename, { ...options, maxDepth: maxDepth - 1 }); + } + } +} diff --git a/std/fs/walk_test.ts b/std/fs/walk_test.ts new file mode 100644 index 000000000..abd5adbcf --- /dev/null +++ b/std/fs/walk_test.ts @@ -0,0 +1,279 @@ +const { cwd, chdir, makeTempDir, mkdir, open, remove } = Deno; +import { walk, walkSync, WalkOptions, WalkInfo } from "./walk.ts"; +import { test, TestFunction, runIfMain } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; + +export async function testWalk( + setup: (arg0: string) => void | Promise<void>, + t: TestFunction +): Promise<void> { + const name = t.name; + async function fn(): Promise<void> { + const origCwd = cwd(); + const d = await makeTempDir(); + chdir(d); + try { + await setup(d); + await t(); + } finally { + chdir(origCwd); + remove(d, { recursive: true }); + } + } + test({ name, fn }); +} + +function normalize({ filename }: WalkInfo): string { + return filename.replace(/\\/g, "/"); +} + +export async function walkArray( + root: string, + options: WalkOptions = {} +): Promise<string[]> { + const arr: string[] = []; + for await (const w of walk(root, { ...options })) { + arr.push(normalize(w)); + } + arr.sort(); // TODO(ry) Remove sort. The order should be deterministic. + const arrSync = Array.from(walkSync(root, options), normalize); + arrSync.sort(); // TODO(ry) Remove sort. The order should be deterministic. + assertEquals(arr, arrSync); + return arr; +} + +export async function touch(path: string): Promise<void> { + await open(path, "w"); +} + +function assertReady(expectedLength: number): void { + const arr = Array.from(walkSync("."), normalize); + + assertEquals(arr.length, expectedLength); +} + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/empty"); + }, + async function emptyDir(): Promise<void> { + const arr = await walkArray("."); + assertEquals(arr, [".", "empty"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x"); + }, + async function singleFile(): Promise<void> { + const arr = await walkArray("."); + assertEquals(arr, [".", "x"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x"); + }, + async function iteratable(): Promise<void> { + let count = 0; + for (const _ of walkSync(".")) { + count += 1; + } + assertEquals(count, 2); + for await (const _ of walk(".")) { + count += 1; + } + assertEquals(count, 4); + } +); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await touch(d + "/a/x"); + }, + async function nestedSingleFile(): Promise<void> { + const arr = await walkArray("."); + assertEquals(arr, [".", "a", "a/x"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a/b/c/d", true); + await touch(d + "/a/b/c/d/x"); + }, + async function depth(): Promise<void> { + assertReady(6); + const arr3 = await walkArray(".", { maxDepth: 3 }); + assertEquals(arr3, [".", "a", "a/b", "a/b/c"]); + const arr5 = await walkArray(".", { maxDepth: 5 }); + assertEquals(arr5, [".", "a", "a/b", "a/b/c", "a/b/c/d", "a/b/c/d/x"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/a"); + await mkdir(d + "/b"); + await touch(d + "/b/c"); + }, + async function includeDirs(): Promise<void> { + assertReady(4); + const arr = await walkArray(".", { includeDirs: false }); + assertEquals(arr, ["a", "b/c"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/a"); + await mkdir(d + "/b"); + await touch(d + "/b/c"); + }, + async function includeFiles(): Promise<void> { + assertReady(4); + const arr = await walkArray(".", { includeFiles: false }); + assertEquals(arr, [".", "b"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x.ts"); + await touch(d + "/y.rs"); + }, + async function ext(): Promise<void> { + assertReady(3); + const arr = await walkArray(".", { exts: [".ts"] }); + assertEquals(arr, ["x.ts"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x.ts"); + await touch(d + "/y.rs"); + await touch(d + "/z.py"); + }, + async function extAny(): Promise<void> { + assertReady(4); + const arr = await walkArray(".", { exts: [".rs", ".ts"] }); + assertEquals(arr, ["x.ts", "y.rs"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x"); + await touch(d + "/y"); + }, + async function match(): Promise<void> { + assertReady(3); + const arr = await walkArray(".", { match: [/x/] }); + assertEquals(arr, ["x"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x"); + await touch(d + "/y"); + await touch(d + "/z"); + }, + async function matchAny(): Promise<void> { + assertReady(4); + const arr = await walkArray(".", { match: [/x/, /y/] }); + assertEquals(arr, ["x", "y"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x"); + await touch(d + "/y"); + }, + async function skip(): Promise<void> { + assertReady(3); + const arr = await walkArray(".", { skip: [/x/] }); + assertEquals(arr, [".", "y"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await touch(d + "/x"); + await touch(d + "/y"); + await touch(d + "/z"); + }, + async function skipAny(): Promise<void> { + assertReady(4); + const arr = await walkArray(".", { skip: [/x/, /y/] }); + assertEquals(arr, [".", "z"]); + } +); + +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await mkdir(d + "/b"); + await touch(d + "/a/x"); + await touch(d + "/a/y"); + await touch(d + "/b/z"); + }, + async function subDir(): Promise<void> { + assertReady(6); + const arr = await walkArray("b"); + assertEquals(arr, ["b", "b/z"]); + } +); + +testWalk( + async (_d: string): Promise<void> => {}, + async function onError(): Promise<void> { + assertReady(1); + const ignored = await walkArray("missing"); + assertEquals(ignored, ["missing"]); + let errors = 0; + await walkArray("missing", { onError: (_e): number => (errors += 1) }); + // It's 2 since walkArray iterates over both sync and async. + assertEquals(errors, 2); + } +); + +/* TODO(ry) Re-enable followSymlinks +testWalk( + async (d: string): Promise<void> => { + await mkdir(d + "/a"); + await mkdir(d + "/b"); + await touch(d + "/a/x"); + await touch(d + "/a/y"); + await touch(d + "/b/z"); + try { + await symlink(d + "/b", d + "/a/bb"); + } catch (err) { + assert(isWindows); + assert(err.message, "Not implemented"); + } + }, + async function symlink(): Promise<void> { + // symlink is not yet implemented on Windows. + if (isWindows) { + return; + } + + assertReady(6); + const files = await walkArray("a"); + assertEquals(files.length, 2); + assert(!files.includes("a/bb/z")); + + const arr = await walkArray("a", { followSymlinks: true }); + assertEquals(arr.length, 3); + assert(arr.some((f): boolean => f.endsWith("/b/z"))); + } +); +*/ + +runIfMain(import.meta); diff --git a/std/fs/write_file_str.ts b/std/fs/write_file_str.ts new file mode 100644 index 000000000..a4a4beb5b --- /dev/null +++ b/std/fs/write_file_str.ts @@ -0,0 +1,28 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +/** + * Write the string to file synchronously. + * + * @param filename File to write + * @param content The content write to file + * @returns void + */ +export function writeFileStrSync(filename: string, content: string): void { + const encoder = new TextEncoder(); + Deno.writeFileSync(filename, encoder.encode(content)); +} + +/** + * Write the string to file. + * + * @param filename File to write + * @param content The content write to file + * @returns Promise<void> + */ +export async function writeFileStr( + filename: string, + content: string +): Promise<void> { + const encoder = new TextEncoder(); + await Deno.writeFile(filename, encoder.encode(content)); +} diff --git a/std/fs/write_file_str_test.ts b/std/fs/write_file_str_test.ts new file mode 100644 index 000000000..77b1e734e --- /dev/null +++ b/std/fs/write_file_str_test.ts @@ -0,0 +1,38 @@ +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { writeFileStr, writeFileStrSync } from "./write_file_str.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(function testReadFileSync(): void { + const jsonFile = path.join(testdataDir, "write_file_1.json"); + const content = "write_file_str_test"; + writeFileStrSync(jsonFile, content); + + // make sure file have been create. + Deno.statSync(jsonFile); + + const result = new TextDecoder().decode(Deno.readFileSync(jsonFile)); + + // remove test file + Deno.removeSync(jsonFile); + + assertEquals(content, result); +}); + +test(async function testReadFile(): Promise<void> { + const jsonFile = path.join(testdataDir, "write_file_2.json"); + const content = "write_file_str_test"; + await writeFileStr(jsonFile, content); + + // make sure file have been create. + await Deno.stat(jsonFile); + + const result = new TextDecoder().decode(await Deno.readFile(jsonFile)); + + // remove test file + await Deno.remove(jsonFile); + + assertEquals(content, result); +}); diff --git a/std/fs/write_json.ts b/std/fs/write_json.ts new file mode 100644 index 000000000..c5936d3f8 --- /dev/null +++ b/std/fs/write_json.ts @@ -0,0 +1,52 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +/* eslint-disable @typescript-eslint/no-explicit-any */ +type Replacer = (key: string, value: any) => any; + +export interface WriteJsonOptions { + spaces?: number | string; + replacer?: Array<number | string> | Replacer; +} + +/* Writes an object to a JSON file. */ +export async function writeJson( + filePath: string, + object: any, + options: WriteJsonOptions = {} +): Promise<void> { + let contentRaw = ""; + + try { + contentRaw = JSON.stringify( + object, + options.replacer as string[], + options.spaces + ); + } catch (err) { + err.message = `${filePath}: ${err.message}`; + throw err; + } + + await Deno.writeFile(filePath, new TextEncoder().encode(contentRaw)); +} + +/* Writes an object to a JSON file. */ +export function writeJsonSync( + filePath: string, + object: any, + options: WriteJsonOptions = {} +): void { + let contentRaw = ""; + + try { + contentRaw = JSON.stringify( + object, + options.replacer as string[], + options.spaces + ); + } catch (err) { + err.message = `${filePath}: ${err.message}`; + throw err; + } + + Deno.writeFileSync(filePath, new TextEncoder().encode(contentRaw)); +} diff --git a/std/fs/write_json_test.ts b/std/fs/write_json_test.ts new file mode 100644 index 000000000..a282d399f --- /dev/null +++ b/std/fs/write_json_test.ts @@ -0,0 +1,244 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { + assertEquals, + assertThrowsAsync, + assertThrows +} from "../testing/asserts.ts"; +import { writeJson, writeJsonSync } from "./write_json.ts"; +import * as path from "./path/mod.ts"; + +const testdataDir = path.resolve("fs", "testdata"); + +test(async function writeJsonIfNotExists(): Promise<void> { + const notExistsJsonFile = path.join(testdataDir, "file_not_exists.json"); + + await assertThrowsAsync( + async (): Promise<void> => { + await writeJson(notExistsJsonFile, { a: "1" }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = await Deno.readFile(notExistsJsonFile); + + await Deno.remove(notExistsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(async function writeJsonIfExists(): Promise<void> { + const existsJsonFile = path.join(testdataDir, "file_write_exists.json"); + + await Deno.writeFile(existsJsonFile, new Uint8Array()); + + await assertThrowsAsync( + async (): Promise<void> => { + await writeJson(existsJsonFile, { a: "1" }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = await Deno.readFile(existsJsonFile); + + await Deno.remove(existsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(async function writeJsonIfExistsAnInvalidJson(): Promise<void> { + const existsInvalidJsonFile = path.join( + testdataDir, + "file_write_invalid.json" + ); + + const invalidJsonContent = new TextEncoder().encode("[123}"); + await Deno.writeFile(existsInvalidJsonFile, invalidJsonContent); + + await assertThrowsAsync( + async (): Promise<void> => { + await writeJson(existsInvalidJsonFile, { a: "1" }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = await Deno.readFile(existsInvalidJsonFile); + + await Deno.remove(existsInvalidJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(async function writeJsonWithSpaces(): Promise<void> { + const existsJsonFile = path.join(testdataDir, "file_write_spaces.json"); + + const invalidJsonContent = new TextEncoder().encode(); + await Deno.writeFile(existsJsonFile, invalidJsonContent); + + await assertThrowsAsync( + async (): Promise<void> => { + await writeJson(existsJsonFile, { a: "1" }, { spaces: 2 }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = await Deno.readFile(existsJsonFile); + + await Deno.remove(existsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{\n "a": "1"\n}`); +}); + +test(async function writeJsonWithReplacer(): Promise<void> { + const existsJsonFile = path.join(testdataDir, "file_write_replacer.json"); + + const invalidJsonContent = new TextEncoder().encode(); + await Deno.writeFile(existsJsonFile, invalidJsonContent); + + await assertThrowsAsync( + async (): Promise<void> => { + await writeJson( + existsJsonFile, + { a: "1", b: "2", c: "3" }, + { + replacer: ["a"] + } + ); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = await Deno.readFile(existsJsonFile); + + await Deno.remove(existsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(function writeJsonSyncIfNotExists(): void { + const notExistsJsonFile = path.join(testdataDir, "file_not_exists_sync.json"); + + assertThrows( + (): void => { + writeJsonSync(notExistsJsonFile, { a: "1" }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = Deno.readFileSync(notExistsJsonFile); + + Deno.removeSync(notExistsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(function writeJsonSyncIfExists(): void { + const existsJsonFile = path.join(testdataDir, "file_write_exists_sync.json"); + + Deno.writeFileSync(existsJsonFile, new Uint8Array()); + + assertThrows( + (): void => { + writeJsonSync(existsJsonFile, { a: "1" }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = Deno.readFileSync(existsJsonFile); + + Deno.removeSync(existsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(function writeJsonSyncIfExistsAnInvalidJson(): void { + const existsInvalidJsonFile = path.join( + testdataDir, + "file_write_invalid_sync.json" + ); + + const invalidJsonContent = new TextEncoder().encode("[123}"); + Deno.writeFileSync(existsInvalidJsonFile, invalidJsonContent); + + assertThrows( + (): void => { + writeJsonSync(existsInvalidJsonFile, { a: "1" }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = Deno.readFileSync(existsInvalidJsonFile); + + Deno.removeSync(existsInvalidJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); + +test(function writeJsonWithSpaces(): void { + const existsJsonFile = path.join(testdataDir, "file_write_spaces_sync.json"); + + const invalidJsonContent = new TextEncoder().encode(); + Deno.writeFileSync(existsJsonFile, invalidJsonContent); + + assertThrows( + (): void => { + writeJsonSync(existsJsonFile, { a: "1" }, { spaces: 2 }); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = Deno.readFileSync(existsJsonFile); + + Deno.removeSync(existsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{\n "a": "1"\n}`); +}); + +test(function writeJsonWithReplacer(): void { + const existsJsonFile = path.join( + testdataDir, + "file_write_replacer_sync.json" + ); + + const invalidJsonContent = new TextEncoder().encode(); + Deno.writeFileSync(existsJsonFile, invalidJsonContent); + + assertThrows( + (): void => { + writeJsonSync( + existsJsonFile, + { a: "1", b: "2", c: "3" }, + { + replacer: ["a"] + } + ); + throw new Error("should write success"); + }, + Error, + "should write success" + ); + + const content = Deno.readFileSync(existsJsonFile); + + Deno.removeSync(existsJsonFile); + + assertEquals(new TextDecoder().decode(content), `{"a":"1"}`); +}); |