1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-26 16:09:27 -05:00

Merge branch 'std_modified' into merge_std3

This commit is contained in:
Ryan Dahl 2019-10-09 17:18:08 -04:00
commit 28293acd9c
No known key found for this signature in database
GPG key ID: C6F2E2494922A6BA
264 changed files with 68231 additions and 0 deletions

50
std/README.md Normal file
View file

@ -0,0 +1,50 @@
# Deno Standard Modules
[![Build Status](https://dev.azure.com/denoland/deno_std/_apis/build/status/denoland.deno_std?branchName=master)](https://dev.azure.com/denoland/deno_std/_build/latest?definitionId=2?branchName=master)
These modules do not have external dependencies and they are reviewed by the
Deno core team. The intention is to have a standard set of high quality code
that all Deno projects can use fearlessly.
Contributions are welcome!
## How to use
These modules are tagged in accordance with Deno releases. So, for example, the
v0.3.0 tag is guaranteed to work with deno v0.3.0.
You can link to v0.3.0 using the URL `https://deno.land/std@v0.3.0/`
It's strongly recommended that you link to tagged releases rather than the
master branch. The project is still young and we expect disruptive renames in
the future.
## Documentation
Here are the dedicated documentations of modules:
- [colors](fmt/colors.ts)
- [datetime](datetime/README.md)
- [encoding](encoding/README.md)
- [examples](examples/README.md)
- [flags](flags/README.md)
- [fs](fs/README.md)
- [http](http/README.md)
- [log](log/README.md)
- [media_types](media_types/README.md)
- [prettier](prettier/README.md)
- [strings](strings/README.md)
- [testing](testing/README.md)
- [uuid](uuid/README.md)
- [ws](ws/README.md)
## Contributing
deno_std is a loose port of [Go's standard library](https://golang.org/pkg/).
When in doubt, simply port Go's source code, documentation, and tests. There
are many times when the nature of JavaScript, TypeScript, or Deno itself
justifies diverging from Go, but if possible we want to leverage the energy that
went into building Go. We generally welcome direct ports of Go's code.
Please ensure the copyright headers cite the code's origin.
Follow the [style guide](https://deno.land/style_guide.html).

488
std/archive/tar.ts Normal file
View file

@ -0,0 +1,488 @@
/**
* Ported and modified from: https://github.com/jshttp/mime-types and
* licensed as:
*
* (The MIT License)
*
* Copyright (c) 2011 T. Jameson Little
* Copyright (c) 2019 Jun Kato
* Copyright (c) 2019 the Deno authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { MultiReader } from "../io/readers.ts";
import { BufReader } from "../io/bufio.ts";
const recordSize = 512;
const ustar = "ustar\u000000";
/**
* Simple file reader
*/
export class FileReader implements Deno.Reader {
private file?: Deno.File;
constructor(private filePath: string, private mode: Deno.OpenMode = "r") {}
public async read(p: Uint8Array): Promise<number | Deno.EOF> {
if (!this.file) {
this.file = await Deno.open(this.filePath, this.mode);
}
const res = await Deno.read(this.file.rid, p);
if (res === Deno.EOF) {
await Deno.close(this.file.rid);
this.file = undefined;
}
return res;
}
}
/**
* Simple file writer (call FileWriter.dispose() after use)
*/
export class FileWriter implements Deno.Writer {
private file?: Deno.File;
constructor(private filePath: string, private mode: Deno.OpenMode = "w") {}
public async write(p: Uint8Array): Promise<number> {
if (!this.file) {
this.file = await Deno.open(this.filePath, this.mode);
}
return Deno.write(this.file.rid, p);
}
public dispose(): void {
if (!this.file) return;
Deno.close(this.file.rid);
this.file = undefined;
}
}
/**
* Remove the trailing null codes
* @param buffer
*/
function trim(buffer: Uint8Array): Uint8Array {
const index = buffer.findIndex((v): boolean => v === 0);
if (index < 0) return buffer;
return buffer.subarray(0, index);
}
/**
* Initialize Uint8Array of the specified length filled with 0
* @param length
*/
function clean(length: number): Uint8Array {
const buffer = new Uint8Array(length);
buffer.fill(0, 0, length - 1);
return buffer;
}
function pad(num: number, bytes: number, base?: number): string {
const numString = num.toString(base || 8);
return "000000000000".substr(numString.length + 12 - bytes) + numString;
}
/*
struct posix_header { // byte offset
char name[100]; // 0
char mode[8]; // 100
char uid[8]; // 108
char gid[8]; // 116
char size[12]; // 124
char mtime[12]; // 136
char chksum[8]; // 148
char typeflag; // 156
char linkname[100]; // 157
char magic[6]; // 257
char version[2]; // 263
char uname[32]; // 265
char gname[32]; // 297
char devmajor[8]; // 329
char devminor[8]; // 337
char prefix[155]; // 345
// 500
};
*/
const ustarStructure: Array<{ field: string; length: number }> = [
{
field: "fileName",
length: 100
},
{
field: "fileMode",
length: 8
},
{
field: "uid",
length: 8
},
{
field: "gid",
length: 8
},
{
field: "fileSize",
length: 12
},
{
field: "mtime",
length: 12
},
{
field: "checksum",
length: 8
},
{
field: "type",
length: 1
},
{
field: "linkName",
length: 100
},
{
field: "ustar",
length: 8
},
{
field: "owner",
length: 32
},
{
field: "group",
length: 32
},
{
field: "majorNumber",
length: 8
},
{
field: "minorNumber",
length: 8
},
{
field: "fileNamePrefix",
length: 155
},
{
field: "padding",
length: 12
}
];
/**
* Create header for a file in a tar archive
*/
function formatHeader(data: TarData): Uint8Array {
const encoder = new TextEncoder(),
buffer = clean(512);
let offset = 0;
ustarStructure.forEach(function(value): void {
const entry = encoder.encode(data[value.field as keyof TarData] || "");
buffer.set(entry, offset);
offset += value.length; // space it out with nulls
});
return buffer;
}
/**
* Parse file header in a tar archive
* @param length
*/
function parseHeader(buffer: Uint8Array): { [key: string]: Uint8Array } {
const data: { [key: string]: Uint8Array } = {};
let offset = 0;
ustarStructure.forEach(function(value): void {
const arr = buffer.subarray(offset, offset + value.length);
data[value.field] = arr;
offset += value.length;
});
return data;
}
export interface TarData {
fileName?: string;
fileNamePrefix?: string;
fileMode?: string;
uid?: string;
gid?: string;
fileSize?: string;
mtime?: string;
checksum?: string;
type?: string;
ustar?: string;
owner?: string;
group?: string;
}
export interface TarDataWithSource extends TarData {
/**
* file to read
*/
filePath?: string;
/**
* buffer to read
*/
reader?: Deno.Reader;
}
export interface TarInfo {
fileMode?: number;
mtime?: number;
uid?: number;
gid?: number;
owner?: string;
group?: string;
}
export interface TarOptions extends TarInfo {
/**
* append file
*/
filePath?: string;
/**
* append any arbitrary content
*/
reader?: Deno.Reader;
/**
* size of the content to be appended
*/
contentSize?: number;
}
export interface UntarOptions extends TarInfo {
fileName: string;
}
/**
* A class to create a tar archive
*/
export class Tar {
data: TarDataWithSource[];
written: number;
out: Uint8Array;
private blockSize: number;
constructor(recordsPerBlock?: number) {
this.data = [];
this.written = 0;
this.blockSize = (recordsPerBlock || 20) * recordSize;
this.out = clean(this.blockSize);
}
/**
* Append a file to this tar archive
* @param fileName file name
* e.g., test.txt; use slash for directory separators
* @param opts options
*/
async append(fileName: string, opts: TarOptions): Promise<void> {
if (typeof fileName !== "string")
throw new Error("file name not specified");
// separate file name into two parts if needed
let fileNamePrefix: string;
if (fileName.length > 100) {
let i = fileName.length;
while (i >= 0) {
i = fileName.lastIndexOf("/", i);
if (i <= 155) {
fileNamePrefix = fileName.substr(0, i);
fileName = fileName.substr(i + 1);
break;
}
i--;
}
if (i < 0 || fileName.length > 100 || fileNamePrefix!.length > 155) {
throw new Error(
"ustar format does not allow a long file name (length of [file name" +
"prefix] + / + [file name] must be shorter than 256 bytes)"
);
}
}
fileNamePrefix = fileNamePrefix!;
opts = opts || {};
// set meta data
const info = opts.filePath && (await Deno.stat(opts.filePath));
const mode =
opts.fileMode || (info && info.mode) || parseInt("777", 8) & 0xfff,
mtime =
opts.mtime ||
(info && info.modified) ||
Math.floor(new Date().getTime() / 1000),
uid = opts.uid || 0,
gid = opts.gid || 0;
if (typeof opts.owner === "string" && opts.owner.length >= 32) {
throw new Error(
"ustar format does not allow owner name length >= 32 bytes"
);
}
if (typeof opts.group === "string" && opts.group.length >= 32) {
throw new Error(
"ustar format does not allow group name length >= 32 bytes"
);
}
const tarData: TarDataWithSource = {
fileName,
fileNamePrefix,
fileMode: pad(mode, 7),
uid: pad(uid, 7),
gid: pad(gid, 7),
fileSize: pad((info ? info.len : opts.contentSize)!, 11),
mtime: pad(mtime, 11),
checksum: " ",
type: "0", // just a file
ustar,
owner: opts.owner || "",
group: opts.group || "",
filePath: opts.filePath,
reader: opts.reader
};
// calculate the checksum
let checksum = 0;
const encoder = new TextEncoder();
Object.keys(tarData)
.filter((key): boolean => ["filePath", "reader"].indexOf(key) < 0)
.forEach(function(key): void {
checksum += encoder
.encode(tarData[key as keyof TarData])
.reduce((p, c): number => p + c, 0);
});
tarData.checksum = pad(checksum, 6) + "\u0000 ";
this.data.push(tarData);
}
/**
* Get a Reader instance for this tar data
*/
getReader(): Deno.Reader {
const readers: Deno.Reader[] = [];
this.data.forEach((tarData): void => {
let { reader } = tarData;
const { filePath } = tarData;
const headerArr = formatHeader(tarData);
readers.push(new Deno.Buffer(headerArr));
if (!reader) {
reader = new FileReader(filePath!);
}
readers.push(reader);
// to the nearest multiple of recordSize
readers.push(
new Deno.Buffer(
clean(
recordSize -
(parseInt(tarData.fileSize!, 8) % recordSize || recordSize)
)
)
);
});
// append 2 empty records
readers.push(new Deno.Buffer(clean(recordSize * 2)));
return new MultiReader(...readers);
}
}
/**
* A class to create a tar archive
*/
export class Untar {
reader: BufReader;
block: Uint8Array;
constructor(reader: Deno.Reader) {
this.reader = new BufReader(reader);
this.block = new Uint8Array(recordSize);
}
async extract(writer: Deno.Writer): Promise<UntarOptions> {
await this.reader.readFull(this.block);
const header = parseHeader(this.block);
// calculate the checksum
let checksum = 0;
const encoder = new TextEncoder(),
decoder = new TextDecoder("ascii");
Object.keys(header)
.filter((key): boolean => key !== "checksum")
.forEach(function(key): void {
checksum += header[key].reduce((p, c): number => p + c, 0);
});
checksum += encoder.encode(" ").reduce((p, c): number => p + c, 0);
if (parseInt(decoder.decode(header.checksum), 8) !== checksum) {
throw new Error("checksum error");
}
const magic = decoder.decode(header.ustar);
if (magic !== ustar) {
throw new Error(`unsupported archive format: ${magic}`);
}
// get meta data
const meta: UntarOptions = {
fileName: decoder.decode(trim(header.fileName))
};
const fileNamePrefix = trim(header.fileNamePrefix);
if (fileNamePrefix.byteLength > 0) {
meta.fileName = decoder.decode(fileNamePrefix) + "/" + meta.fileName;
}
(["fileMode", "mtime", "uid", "gid"] as [
"fileMode",
"mtime",
"uid",
"gid"
]).forEach((key): void => {
const arr = trim(header[key]);
if (arr.byteLength > 0) {
meta[key] = parseInt(decoder.decode(arr), 8);
}
});
(["owner", "group"] as ["owner", "group"]).forEach((key): void => {
const arr = trim(header[key]);
if (arr.byteLength > 0) {
meta[key] = decoder.decode(arr);
}
});
// read the file content
const len = parseInt(decoder.decode(header.fileSize), 8);
let rest = len;
while (rest > 0) {
await this.reader.readFull(this.block);
const arr = rest < recordSize ? this.block.subarray(0, rest) : this.block;
await Deno.copy(writer, new Deno.Buffer(arr));
rest -= recordSize;
}
return meta;
}
}

91
std/archive/tar_test.ts Normal file
View file

@ -0,0 +1,91 @@
/**
* Tar test
*
* **test summary**
* - create a tar archive in memory containing output.txt and dir/tar.ts.
* - read and deflate a tar archive containing output.txt
*
* **to run this test**
* deno run --allow-read archive/tar_test.ts
*/
import { test, runIfMain } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { Tar, Untar } from "./tar.ts";
import { resolve } from "../fs/path/mod.ts";
const filePath = resolve("archive", "testdata", "example.txt");
test(async function createTarArchive(): Promise<void> {
// initialize
const tar = new Tar();
// put data on memory
const content = new TextEncoder().encode("hello tar world!");
await tar.append("output.txt", {
reader: new Deno.Buffer(content),
contentSize: content.byteLength
});
// put a file
await tar.append("dir/tar.ts", { filePath });
// write tar data to a buffer
const writer = new Deno.Buffer(),
wrote = await Deno.copy(writer, tar.getReader());
/**
* 3072 = 512 (header) + 512 (content) + 512 (header) + 512 (content)
* + 1024 (footer)
*/
assertEquals(wrote, 3072);
});
test(async function deflateTarArchive(): Promise<void> {
const fileName = "output.txt";
const text = "hello tar world!";
// create a tar archive
const tar = new Tar();
const content = new TextEncoder().encode(text);
await tar.append(fileName, {
reader: new Deno.Buffer(content),
contentSize: content.byteLength
});
// read data from a tar archive
const untar = new Untar(tar.getReader());
const buf = new Deno.Buffer();
const result = await untar.extract(buf);
const untarText = new TextDecoder("utf-8").decode(buf.bytes());
// tests
assertEquals(result.fileName, fileName);
assertEquals(untarText, text);
});
test(async function appendFileWithLongNameToTarArchive(): Promise<void> {
// 9 * 15 + 13 = 148 bytes
const fileName = new Array(10).join("long-file-name/") + "file-name.txt";
const text = "hello tar world!";
// create a tar archive
const tar = new Tar();
const content = new TextEncoder().encode(text);
await tar.append(fileName, {
reader: new Deno.Buffer(content),
contentSize: content.byteLength
});
// read data from a tar archive
const untar = new Untar(tar.getReader());
const buf = new Deno.Buffer();
const result = await untar.extract(buf);
const untarText = new TextDecoder("utf-8").decode(buf.bytes());
// tests
assertEquals(result.fileName, fileName);
assertEquals(untarText, text);
});
runIfMain(import.meta);

1
std/archive/testdata/example.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

16
std/bundle/README.md Normal file
View file

@ -0,0 +1,16 @@
# bundle
These are modules that help support bundling with Deno.
## Usage
The main usage is to load and run bundles. For example, to run a bundle named
`bundle.js` in your current working directory:
```sh
deno run https://deno.land/std/bundle/run.ts bundle.js
```
---
Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.

11
std/bundle/run.ts Normal file
View file

@ -0,0 +1,11 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { evaluate, instantiate, load } from "./utils.ts";
async function main(args: string[]): Promise<void> {
const text = await load(args);
const result = evaluate(text);
instantiate(...result);
}
main(Deno.args);

116
std/bundle/test.ts Normal file
View file

@ -0,0 +1,116 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import {
assert,
AssertionError,
assertEquals,
assertThrowsAsync
} from "../testing/asserts.ts";
import { instantiate, load, ModuleMetaData } from "./utils.ts";
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace globalThis {
// eslint-disable-next-line no-var
var __results: [string, string] | undefined;
}
}
/* eslint-disable max-len */
/* eslint-enable @typescript-eslint/no-namespace */
/*
const fixture = `
define("data", [], { "baz": "qat" });
define("modB", ["require", "exports", "data"], function(require, exports, data) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.foo = "bar";
exports.baz = data.baz;
});
define("modA", ["require", "exports", "modB"], function(require, exports, modB) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
globalThis.__results = [modB.foo, modB.baz];
});
`;
*/
/* eslint-enable max-len */
const fixtureQueue = ["data", "modB", "modA"];
const fixtureModules = new Map<string, ModuleMetaData>();
fixtureModules.set("data", {
dependencies: [],
factory: {
baz: "qat"
},
exports: {}
});
fixtureModules.set("modB", {
dependencies: ["require", "exports", "data"],
factory(_require, exports, data): void {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.foo = "bar";
exports.baz = data.baz;
},
exports: {}
});
fixtureModules.set("modA", {
dependencies: ["require", "exports", "modB"],
factory(_require, exports, modB): void {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
globalThis.__results = [modB.foo, modB.baz];
},
exports: {}
});
test(async function loadBundle(): Promise<void> {
const result = await load(["", "./bundle/testdata/bundle.js", "--foo"]);
assert(result != null);
assert(
result.includes(
`define("subdir/print_hello", ["require", "exports"], function(`
)
);
});
test(async function loadBadArgs(): Promise<void> {
await assertThrowsAsync(
async (): Promise<void> => {
await load(["bundle/test.ts"]);
},
AssertionError,
"Expected at least two arguments."
);
});
test(async function loadMissingBundle(): Promise<void> {
await assertThrowsAsync(
async (): Promise<void> => {
await load([".", "bad_bundle.js"]);
},
AssertionError,
`Expected "bad_bundle.js" to exist.`
);
});
/* TODO re-enable test
test(async function evaluateBundle(): Promise<void> {
assert(globalThis.define == null, "Expected 'define' to be undefined");
const [queue, modules] = evaluate(fixture);
assert(globalThis.define == null, "Expected 'define' to be undefined");
assertEquals(queue, ["data", "modB", "modA"]);
assert(modules.has("modA"));
assert(modules.has("modB"));
assert(modules.has("data"));
assertStrictEq(modules.size, 3);
});
*/
test(async function instantiateBundle(): Promise<void> {
assert(globalThis.__results == null);
instantiate(fixtureQueue, fixtureModules);
assertEquals(globalThis.__results, ["bar", "qat"]);
delete globalThis.__results;
});

67
std/bundle/testdata/bundle.js vendored Normal file
View file

@ -0,0 +1,67 @@
define("subdir/print_hello", ["require", "exports"], function(
require,
exports
) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function printHello() {
console.log("Hello");
}
exports.printHello = printHello;
});
define("subdir/subdir2/mod2", [
"require",
"exports",
"subdir/print_hello"
], function(require, exports, print_hello_ts_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function returnsFoo() {
return "Foo";
}
exports.returnsFoo = returnsFoo;
function printHello2() {
print_hello_ts_1.printHello();
}
exports.printHello2 = printHello2;
});
define("subdir/mod1", ["require", "exports", "subdir/subdir2/mod2"], function(
require,
exports,
mod2_ts_1
) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function returnsHi() {
return "Hi";
}
exports.returnsHi = returnsHi;
function returnsFoo2() {
return mod2_ts_1.returnsFoo();
}
exports.returnsFoo2 = returnsFoo2;
function printHello3() {
mod2_ts_1.printHello2();
}
exports.printHello3 = printHello3;
function throwsError() {
throw Error("exception from mod1");
}
exports.throwsError = throwsError;
});
define("005_more_imports", ["require", "exports", "subdir/mod1"], function(
require,
exports,
mod1_ts_1
) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
mod1_ts_1.printHello3();
if (mod1_ts_1.returnsHi() !== "Hi") {
throw Error("Unexpected");
}
if (mod1_ts_1.returnsFoo2() !== "Foo") {
throw Error("Unexpected");
}
});
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZmlsZTovLy9Vc2Vycy9ra2VsbHkvZ2l0aHViL2Rlbm8vdGVzdHMvc3ViZGlyL3ByaW50X2hlbGxvLnRzIiwiZmlsZTovLy9Vc2Vycy9ra2VsbHkvZ2l0aHViL2Rlbm8vdGVzdHMvc3ViZGlyL3N1YmRpcjIvbW9kMi50cyIsImZpbGU6Ly8vVXNlcnMva2tlbGx5L2dpdGh1Yi9kZW5vL3Rlc3RzL3N1YmRpci9tb2QxLnRzIiwiZmlsZTovLy9Vc2Vycy9ra2VsbHkvZ2l0aHViL2Rlbm8vdGVzdHMvMDA1X21vcmVfaW1wb3J0cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7SUFBQSxTQUFnQixVQUFVO1FBQ3hCLE9BQU8sQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdkIsQ0FBQztJQUZELGdDQUVDOzs7OztJQ0FELFNBQWdCLFVBQVU7UUFDeEIsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRkQsZ0NBRUM7SUFFRCxTQUFnQixXQUFXO1FBQ3pCLDJCQUFVLEVBQUUsQ0FBQztJQUNmLENBQUM7SUFGRCxrQ0FFQzs7Ozs7SUNORCxTQUFnQixTQUFTO1FBQ3ZCLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUZELDhCQUVDO0lBRUQsU0FBZ0IsV0FBVztRQUN6QixPQUFPLG9CQUFVLEVBQUUsQ0FBQztJQUN0QixDQUFDO0lBRkQsa0NBRUM7SUFFRCxTQUFnQixXQUFXO1FBQ3pCLHFCQUFXLEVBQUUsQ0FBQztJQUNoQixDQUFDO0lBRkQsa0NBRUM7SUFFRCxTQUFnQixXQUFXO1FBQ3pCLE1BQU0sS0FBSyxDQUFDLHFCQUFxQixDQUFDLENBQUM7SUFDckMsQ0FBQztJQUZELGtDQUVDOzs7OztJQ2RELHFCQUFXLEVBQUUsQ0FBQztJQUVkLElBQUksbUJBQVMsRUFBRSxLQUFLLElBQUksRUFBRTtRQUN4QixNQUFNLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQztLQUMzQjtJQUVELElBQUkscUJBQVcsRUFBRSxLQUFLLEtBQUssRUFBRTtRQUMzQixNQUFNLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQztLQUMzQiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBwcmludEhlbGxvKCk6IHZvaWQge1xuICBjb25zb2xlLmxvZyhcIkhlbGxvXCIpO1xufVxuIiwiaW1wb3J0IHsgcHJpbnRIZWxsbyB9IGZyb20gXCIuLi9wcmludF9oZWxsby50c1wiO1xuXG5leHBvcnQgZnVuY3Rpb24gcmV0dXJuc0ZvbygpOiBzdHJpbmcge1xuICByZXR1cm4gXCJGb29cIjtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHByaW50SGVsbG8yKCk6IHZvaWQge1xuICBwcmludEhlbGxvKCk7XG59XG4iLCJpbXBvcnQgeyByZXR1cm5zRm9vLCBwcmludEhlbGxvMiB9IGZyb20gXCIuL3N1YmRpcjIvbW9kMi50c1wiO1xuXG5leHBvcnQgZnVuY3Rpb24gcmV0dXJuc0hpKCk6IHN0cmluZyB7XG4gIHJldHVybiBcIkhpXCI7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZXR1cm5zRm9vMigpOiBzdHJpbmcge1xuICByZXR1cm4gcmV0dXJuc0ZvbygpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcHJpbnRIZWxsbzMoKTogdm9pZCB7XG4gIHByaW50SGVsbG8yKCk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiB0aHJvd3NFcnJvcigpOiB2b2lkIHtcbiAgdGhyb3cgRXJyb3IoXCJleGNlcHRpb24gZnJvbSBtb2QxXCIpO1xufVxuIiwiaW1wb3J0IHsgcmV0dXJuc0hpLCByZXR1cm5zRm9vMiwgcHJpbnRIZWxsbzMgfSBmcm9tIFwiLi9zdWJkaXIvbW9kMS50c1wiO1xuXG5wcmludEhlbGxvMygpO1xuXG5pZiAocmV0dXJuc0hpKCkgIT09IFwiSGlcIikge1xuICB0aHJvdyBFcnJvcihcIlVuZXhwZWN0ZWRcIik7XG59XG5cbmlmIChyZXR1cm5zRm9vMigpICE9PSBcIkZvb1wiKSB7XG4gIHRocm93IEVycm9yKFwiVW5leHBlY3RlZFwiKTtcbn1cbiJdfQ==

107
std/bundle/utils.ts Normal file
View file

@ -0,0 +1,107 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { assert } from "../testing/asserts.ts";
import { exists } from "../fs/exists.ts";
export interface DefineFactory {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
(...args: any): object | void;
}
export interface ModuleMetaData {
dependencies: string[];
factory?: DefineFactory | object;
exports: object;
}
type Define = (
id: string,
dependencies: string[],
factory: DefineFactory
) => void;
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace globalThis {
// eslint-disable-next-line no-var
var define: Define | undefined;
}
}
/* eslint-enable @typescript-eslint/no-namespace */
/** Evaluate the bundle, returning a queue of module IDs and their data to
* instantiate.
*/
export function evaluate(
text: string
): [string[], Map<string, ModuleMetaData>] {
const queue: string[] = [];
const modules = new Map<string, ModuleMetaData>();
globalThis.define = function define(
id: string,
dependencies: string[],
factory: DefineFactory
): void {
modules.set(id, {
dependencies,
factory,
exports: {}
});
queue.push(id);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Deno as any).core.evalContext(text);
// Deleting `define()` so it isn't accidentally there when the modules
// instantiate.
delete globalThis.define;
return [queue, modules];
}
/** Drain the queue of module IDs while instantiating the modules. */
export function instantiate(
queue: string[],
modules: Map<string, ModuleMetaData>
): void {
let id: string | undefined;
while ((id = queue.shift())) {
const module = modules.get(id)!;
assert(module != null);
assert(module.factory != null);
const dependencies = module.dependencies.map((id): object => {
if (id === "require") {
// TODO(kitsonk) support dynamic import by passing a `require()` that
// can return a local module or dynamically import one.
return (): void => {};
} else if (id === "exports") {
return module.exports;
}
const dep = modules.get(id)!;
assert(dep != null);
return dep.exports;
});
if (typeof module.factory === "function") {
module.factory!(...dependencies);
} else if (module.factory) {
// when bundling JSON, TypeScript just emits it as an object/array as the
// third argument of the `define()`.
module.exports = module.factory;
}
delete module.factory;
}
}
/** Load the bundle and return the contents asynchronously. */
export async function load(args: string[]): Promise<string> {
// TODO(kitsonk) allow loading of remote bundles via fetch.
assert(args.length >= 2, "Expected at least two arguments.");
const [, bundleFileName] = args;
assert(
await exists(bundleFileName),
`Expected "${bundleFileName}" to exist.`
);
return new TextDecoder().decode(await Deno.readFile(bundleFileName));
}

96
std/bytes/mod.ts Normal file
View file

@ -0,0 +1,96 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { copyBytes } from "../io/util.ts";
/** Find first index of binary pattern from a. If not found, then return -1 **/
export function findIndex(a: Uint8Array, pat: Uint8Array): number {
const s = pat[0];
for (let i = 0; i < a.length; i++) {
if (a[i] !== s) continue;
const pin = i;
let matched = 1,
j = i;
while (matched < pat.length) {
j++;
if (a[j] !== pat[j - pin]) {
break;
}
matched++;
}
if (matched === pat.length) {
return pin;
}
}
return -1;
}
/** Find last index of binary pattern from a. If not found, then return -1 **/
export function findLastIndex(a: Uint8Array, pat: Uint8Array): number {
const e = pat[pat.length - 1];
for (let i = a.length - 1; i >= 0; i--) {
if (a[i] !== e) continue;
const pin = i;
let matched = 1,
j = i;
while (matched < pat.length) {
j--;
if (a[j] !== pat[pat.length - 1 - (pin - j)]) {
break;
}
matched++;
}
if (matched === pat.length) {
return pin - pat.length + 1;
}
}
return -1;
}
/** Check whether binary arrays are equal to each other **/
export function equal(a: Uint8Array, match: Uint8Array): boolean {
if (a.length !== match.length) return false;
for (let i = 0; i < match.length; i++) {
if (a[i] !== match[i]) return false;
}
return true;
}
/** Check whether binary array has binary prefix **/
export function hasPrefix(a: Uint8Array, prefix: Uint8Array): boolean {
for (let i = 0, max = prefix.length; i < max; i++) {
if (a[i] !== prefix[i]) return false;
}
return true;
}
/**
* Repeat bytes. returns a new byte slice consisting of `count` copies of `b`.
* @param b The origin bytes
* @param count The count you want to repeat.
*/
export function repeat(b: Uint8Array, count: number): Uint8Array {
if (count === 0) {
return new Uint8Array();
}
if (count < 0) {
throw new Error("bytes: negative repeat count");
} else if ((b.length * count) / count !== b.length) {
throw new Error("bytes: repeat count causes overflow");
}
const int = Math.floor(count);
if (int !== count) {
throw new Error("bytes: repeat count must be an integer");
}
const nb = new Uint8Array(b.length * count);
let bp = copyBytes(nb, b);
for (; bp < nb.length; bp *= 2) {
copyBytes(nb, nb.slice(0, bp), bp);
}
return nb;
}

74
std/bytes/test.ts Normal file
View file

@ -0,0 +1,74 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { findIndex, findLastIndex, equal, hasPrefix, repeat } from "./mod.ts";
import { test } from "../testing/mod.ts";
import { assertEquals, assertThrows } from "../testing/asserts.ts";
test(function bytesfindIndex1(): void {
const i = findIndex(
new Uint8Array([1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 3]),
new Uint8Array([0, 1, 2])
);
assertEquals(i, 2);
});
test(function bytesfindIndex2(): void {
const i = findIndex(new Uint8Array([0, 0, 1]), new Uint8Array([0, 1]));
assertEquals(i, 1);
});
test(function bytesfindLastIndex1(): void {
const i = findLastIndex(
new Uint8Array([0, 1, 2, 0, 1, 2, 0, 1, 3]),
new Uint8Array([0, 1, 2])
);
assertEquals(i, 3);
});
test(function bytesfindLastIndex2(): void {
const i = findLastIndex(new Uint8Array([0, 1, 1]), new Uint8Array([0, 1]));
assertEquals(i, 0);
});
test(function bytesBytesequal(): void {
const v = equal(new Uint8Array([0, 1, 2, 3]), new Uint8Array([0, 1, 2, 3]));
assertEquals(v, true);
});
test(function byteshasPrefix(): void {
const v = hasPrefix(new Uint8Array([0, 1, 2]), new Uint8Array([0, 1]));
assertEquals(v, true);
});
test(function bytesrepeat(): void {
// input / output / count / error message
const repeatTestCase = [
["", "", 0],
["", "", 1],
["", "", 1.1, "bytes: repeat count must be an integer"],
["", "", 2],
["", "", 0],
["-", "", 0],
["-", "-", -1, "bytes: negative repeat count"],
["-", "----------", 10],
["abc ", "abc abc abc ", 3]
];
for (const [input, output, count, errMsg] of repeatTestCase) {
if (errMsg) {
assertThrows(
(): void => {
repeat(new TextEncoder().encode(input as string), count as number);
},
Error,
errMsg as string
);
} else {
const newBytes = repeat(
new TextEncoder().encode(input as string),
count as number
);
assertEquals(new TextDecoder().decode(newBytes), output);
}
}
});

37
std/datetime/README.md Normal file
View file

@ -0,0 +1,37 @@
# datetime
Simple helper to help parse date strings into `Date`, with additional functions.
## Usage
### parseDate / parseDateTime
- `parseDate()` - Take an input string and a format to parse the date. Supported formats are exported in `DateFormat`.
- `parseDateTime()` - Take an input string and a format to parse the dateTime. Supported formats are exported in `DateTimeFormat`.
```ts
import { parseDate, parseDateTime } from 'https://deno.land/std/datetime/mod.ts'
parseDate("03-01-2019", "dd-mm-yyyy") // output : new Date(2019, 1, 3)
parseDate("2019-01-03", "yyyy-mm-dd") // output : new Date(2019, 1, 3)
...
parseDateTime("01-03-2019 16:34", "mm-dd-yyyy hh:mm") // output : new Date(2019, 1, 3, 16, 34)
parseDateTime("16:34 01-03-2019", "hh:mm mm-dd-yyyy") // output : new Date(2019, 1, 3, 16, 34)
...
```
### dayOfYear / currentDayOfYear
- `dayOfYear()` - Returns the number of the day in the year.
- `currentDayOfYear()` - Returns the number of the current day in the year.
```ts
import {
dayOfYear,
currentDayOfYear
} from "https://deno.land/std/datetime/mod.ts";
dayOfYear(new Date("2019-03-11T03:24:00")); // output: 70
currentDayOfYear(); // output: ** depends on when you run it :) **
```

146
std/datetime/mod.ts Normal file
View file

@ -0,0 +1,146 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { pad } from "../strings/pad.ts";
export type DateFormat = "mm-dd-yyyy" | "dd-mm-yyyy" | "yyyy-mm-dd";
/**
* Parse date from string using format string
* @param dateStr Date string
* @param format Format string
* @return Parsed date
*/
export function parseDate(dateStr: string, format: DateFormat): Date {
let m, d, y: string;
let datePattern: RegExp;
switch (format) {
case "mm-dd-yyyy":
datePattern = /^(\d{2})-(\d{2})-(\d{4})$/;
[, m, d, y] = datePattern.exec(dateStr)!;
break;
case "dd-mm-yyyy":
datePattern = /^(\d{2})-(\d{2})-(\d{4})$/;
[, d, m, y] = datePattern.exec(dateStr)!;
break;
case "yyyy-mm-dd":
datePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
[, y, m, d] = datePattern.exec(dateStr)!;
break;
default:
throw new Error("Invalid date format!");
}
return new Date(Number(y), Number(m) - 1, Number(d));
}
export type DateTimeFormat =
| "mm-dd-yyyy hh:mm"
| "dd-mm-yyyy hh:mm"
| "yyyy-mm-dd hh:mm"
| "hh:mm mm-dd-yyyy"
| "hh:mm dd-mm-yyyy"
| "hh:mm yyyy-mm-dd";
/**
* Parse date & time from string using format string
* @param dateStr Date & time string
* @param format Format string
* @return Parsed date
*/
export function parseDateTime(
datetimeStr: string,
format: DateTimeFormat
): Date {
let m, d, y, ho, mi: string;
let datePattern: RegExp;
switch (format) {
case "mm-dd-yyyy hh:mm":
datePattern = /^(\d{2})-(\d{2})-(\d{4}) (\d{2}):(\d{2})$/;
[, m, d, y, ho, mi] = datePattern.exec(datetimeStr)!;
break;
case "dd-mm-yyyy hh:mm":
datePattern = /^(\d{2})-(\d{2})-(\d{4}) (\d{2}):(\d{2})$/;
[, d, m, y, ho, mi] = datePattern.exec(datetimeStr)!;
break;
case "yyyy-mm-dd hh:mm":
datePattern = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/;
[, y, m, d, ho, mi] = datePattern.exec(datetimeStr)!;
break;
case "hh:mm mm-dd-yyyy":
datePattern = /^(\d{2}):(\d{2}) (\d{2})-(\d{2})-(\d{4})$/;
[, ho, mi, m, d, y] = datePattern.exec(datetimeStr)!;
break;
case "hh:mm dd-mm-yyyy":
datePattern = /^(\d{2}):(\d{2}) (\d{2})-(\d{2})-(\d{4})$/;
[, ho, mi, d, m, y] = datePattern.exec(datetimeStr)!;
break;
case "hh:mm yyyy-mm-dd":
datePattern = /^(\d{2}):(\d{2}) (\d{4})-(\d{2})-(\d{2})$/;
[, ho, mi, y, m, d] = datePattern.exec(datetimeStr)!;
break;
default:
throw new Error("Invalid datetime format!");
}
return new Date(Number(y), Number(m) - 1, Number(d), Number(ho), Number(mi));
}
/**
* Get number of the day in the year
* @return Number of the day in year
*/
export function dayOfYear(date: Date): number {
const dayMs = 1000 * 60 * 60 * 24;
const yearStart = new Date(date.getFullYear(), 0, 0);
const diff =
date.getTime() -
yearStart.getTime() +
(yearStart.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000;
return Math.floor(diff / dayMs);
}
/**
* Get number of current day in year
* @return Number of current day in year
*/
export function currentDayOfYear(): number {
return dayOfYear(new Date());
}
/**
* Parse a date to return a IMF formated string date
* RFC: https://tools.ietf.org/html/rfc7231#section-7.1.1.1
* IMF is the time format to use when generating times in HTTP
* headers. The time being formatted must be in UTC for Format to
* generate the correct format.
* @param date Date to parse
* @return IMF date formated string
*/
export function toIMF(date: Date): string {
function dtPad(v: string, lPad = 2): string {
return pad(v, lPad, { char: "0" });
}
const d = dtPad(date.getUTCDate().toString());
const h = dtPad(date.getUTCHours().toString());
const min = dtPad(date.getUTCMinutes().toString());
const s = dtPad(date.getUTCSeconds().toString());
const y = date.getUTCFullYear();
const days = ["Sun", "Mon", "Tue", "Wed", "Thus", "Fri", "Sat"];
const months = [
"Jan",
"Feb",
"Mar",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
return `${days[date.getUTCDay()]}, ${d} ${
months[date.getUTCMonth()]
} ${y} ${h}:${min}:${s} GMT`;
}

96
std/datetime/test.ts Normal file
View file

@ -0,0 +1,96 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals, assertThrows } from "../testing/asserts.ts";
import * as datetime from "./mod.ts";
test(function parseDateTime(): void {
assertEquals(
datetime.parseDateTime("01-03-2019 16:30", "mm-dd-yyyy hh:mm"),
new Date(2019, 0, 3, 16, 30)
);
assertEquals(
datetime.parseDateTime("03-01-2019 16:31", "dd-mm-yyyy hh:mm"),
new Date(2019, 0, 3, 16, 31)
);
assertEquals(
datetime.parseDateTime("2019-01-03 16:32", "yyyy-mm-dd hh:mm"),
new Date(2019, 0, 3, 16, 32)
);
assertEquals(
datetime.parseDateTime("16:33 01-03-2019", "hh:mm mm-dd-yyyy"),
new Date(2019, 0, 3, 16, 33)
);
assertEquals(
datetime.parseDateTime("16:34 03-01-2019", "hh:mm dd-mm-yyyy"),
new Date(2019, 0, 3, 16, 34)
);
assertEquals(
datetime.parseDateTime("16:35 2019-01-03", "hh:mm yyyy-mm-dd"),
new Date(2019, 0, 3, 16, 35)
);
});
test(function invalidParseDateTimeFormatThrows(): void {
assertThrows(
(): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(datetime as any).parseDateTime("2019-01-01 00:00", "x-y-z");
},
Error,
"Invalid datetime format!"
);
});
test(function parseDate(): void {
assertEquals(
datetime.parseDate("01-03-2019", "mm-dd-yyyy"),
new Date(2019, 0, 3)
);
assertEquals(
datetime.parseDate("03-01-2019", "dd-mm-yyyy"),
new Date(2019, 0, 3)
);
assertEquals(
datetime.parseDate("2019-01-03", "yyyy-mm-dd"),
new Date(2019, 0, 3)
);
});
test(function invalidParseDateFormatThrows(): void {
assertThrows(
(): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(datetime as any).parseDate("2019-01-01", "x-y-z");
},
Error,
"Invalid date format!"
);
});
test(function DayOfYear(): void {
assertEquals(1, datetime.dayOfYear(new Date("2019-01-01T03:24:00")));
assertEquals(70, datetime.dayOfYear(new Date("2019-03-11T03:24:00")));
assertEquals(365, datetime.dayOfYear(new Date("2019-12-31T03:24:00")));
});
test(function currentDayOfYear(): void {
assertEquals(datetime.currentDayOfYear(), datetime.dayOfYear(new Date()));
});
test({
name: "[DateTime] to IMF",
fn(): void {
const actual = datetime.toIMF(new Date(Date.UTC(1994, 3, 5, 15, 32)));
const expected = "Tue, 05 May 1994 15:32:00 GMT";
assertEquals(actual, expected);
}
});
test({
name: "[DateTime] to IMF 0",
fn(): void {
const actual = datetime.toIMF(new Date(0));
const expected = "Thus, 01 Jan 1970 00:00:00 GMT";
assertEquals(actual, expected);
}
});

219
std/encoding/README.md Normal file
View file

@ -0,0 +1,219 @@
# Encoding
## CSV
- **`readAll(reader: BufReader, opt: ParseOptions = { comma: ",", trimLeadingSpace: false, lazyQuotes: false } ): Promise<[string[][], BufState]>`**:
Read the whole buffer and output the structured CSV datas
- **`parse(csvString: string, opt: ParseOption): Promise<unknown[]>`**:
See [parse](###Parse)
### Parse
Parse the CSV string with the options provided.
#### Options
##### ParseOption
- **`header: boolean | string[] | HeaderOption[];`**: If a boolean is provided,
the first line will be used as Header definitions. If `string[]` or
`HeaderOption[]`
those names will be used for header definition.
- **`parse?: (input: unknown) => unknown;`**: Parse function for the row, which
will be executed after parsing of all columns. Therefore if you don't provide
header and parse function with headers, input will be `string[]`.
##### HeaderOption
- **`name: string;`**: Name of the header to be used as property.
- **`parse?: (input: string) => unknown;`**: Parse function for the column.
This is executed on each entry of the header. This can be combined with the
Parse function of the rows.
#### Usage
```ts
// input:
// a,b,c
// e,f,g
const r = await parseFile(filepath, {
header: false
});
// output:
// [["a", "b", "c"], ["e", "f", "g"]]
const r = await parseFile(filepath, {
header: true
});
// output:
// [{ a: "e", b: "f", c: "g" }]
const r = await parseFile(filepath, {
header: ["this", "is", "sparta"]
});
// output:
// [
// { this: "a", is: "b", sparta: "c" },
// { this: "e", is: "f", sparta: "g" }
// ]
const r = await parseFile(filepath, {
header: [
{
name: "this",
parse: (e: string): string => {
return `b${e}$$`;
}
},
{
name: "is",
parse: (e: string): number => {
return e.length;
}
},
{
name: "sparta",
parse: (e: string): unknown => {
return { bim: `boom-${e}` };
}
}
]
});
// output:
// [
// { this: "ba$$", is: 1, sparta: { bim: `boom-c` } },
// { this: "be$$", is: 1, sparta: { bim: `boom-g` } }
// ]
const r = await parseFile(filepath, {
header: ["this", "is", "sparta"],
parse: (e: Record<string, unknown>) => {
return { super: e.this, street: e.is, fighter: e.sparta };
}
});
// output:
// [
// { super: "a", street: "b", fighter: "c" },
// { super: "e", street: "f", fighter: "g" }
// ]
```
## TOML
This module parse TOML files. It follows as much as possible the
[TOML specs](https://github.com/toml-lang/toml). Be sure to read the supported
types as not every specs is supported at the moment and the handling in
TypeScript side is a bit different.
### Supported types and handling
- :heavy_check_mark: [Keys](https://github.com/toml-lang/toml#string)
- :exclamation: [String](https://github.com/toml-lang/toml#string)
- :heavy_check_mark:
[Multiline String](https://github.com/toml-lang/toml#string)
- :heavy_check_mark: [Literal String](https://github.com/toml-lang/toml#string)
- :exclamation: [Integer](https://github.com/toml-lang/toml#integer)
- :heavy_check_mark: [Float](https://github.com/toml-lang/toml#float)
- :heavy_check_mark: [Boolean](https://github.com/toml-lang/toml#boolean)
- :heavy_check_mark:
[Offset Date-time](https://github.com/toml-lang/toml#offset-date-time)
- :heavy_check_mark:
[Local Date-time](https://github.com/toml-lang/toml#local-date-time)
- :heavy_check_mark: [Local Date](https://github.com/toml-lang/toml#local-date)
- :exclamation: [Local Time](https://github.com/toml-lang/toml#local-time)
- :heavy_check_mark: [Table](https://github.com/toml-lang/toml#table)
- :heavy_check_mark: [Inline Table](https://github.com/toml-lang/toml#inline-table)
- :exclamation: [Array of Tables](https://github.com/toml-lang/toml#array-of-tables)
:exclamation: _Supported with warnings see [Warning](#Warning)._
#### :warning: Warning
##### String
- Regex : Due to the spec, there is no flag to detect regex properly
in a TOML declaration. So the regex is stored as string.
##### Integer
For **Binary** / **Octal** / **Hexadecimal** numbers,
they are stored as string to be not interpreted as Decimal.
##### Local Time
Because local time does not exist in JavaScript, the local time is stored as a string.
##### Inline Table
Inline tables are supported. See below:
```toml
animal = { type = { name = "pug" } }
## Output
animal = { type.name = "pug" }
## Output { animal : { type : { name : "pug" } }
animal.as.leaders = "tosin"
## Output { animal: { as: { leaders: "tosin" } } }
"tosin.abasi" = "guitarist"
## Output
"tosin.abasi" : "guitarist"
```
##### Array of Tables
At the moment only simple declarations like below are supported:
```toml
[[bin]]
name = "deno"
path = "cli/main.rs"
[[bin]]
name = "deno_core"
path = "src/foo.rs"
[[nib]]
name = "node"
path = "not_found"
```
will output:
```json
{
"bin": [
{ "name": "deno", "path": "cli/main.rs" },
{ "name": "deno_core", "path": "src/foo.rs" }
],
"nib": [{ "name": "node", "path": "not_found" }]
}
```
### Usage
#### Parse
```ts
import { parse } from "./parser.ts";
import { readFileStrSync } from "../fs/read_file_str.ts";
const tomlObject = parse(readFileStrSync("file.toml"));
const tomlString = 'foo.bar = "Deno"';
const tomlObject22 = parse(tomlString);
```
#### Stringify
```ts
import { stringify } from "./parser.ts";
const obj = {
bin: [
{ name: "deno", path: "cli/main.rs" },
{ name: "deno_core", path: "src/foo.rs" }
],
nib: [{ name: "node", path: "not_found" }]
};
const tomlString = stringify(obj);
```

251
std/encoding/csv.ts Normal file
View file

@ -0,0 +1,251 @@
// Ported from Go:
// https://github.com/golang/go/blob/go1.12.5/src/encoding/csv/
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { BufReader } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts";
import { StringReader } from "../io/readers.ts";
const INVALID_RUNE = ["\r", "\n", '"'];
export class ParseError extends Error {
StartLine: number;
Line: number;
constructor(start: number, line: number, message: string) {
super(message);
this.StartLine = start;
this.Line = line;
}
}
/**
* @property comma - Character which separates values. Default: ','
* @property comment - Character to start a comment. Default: '#'
* @property trimLeadingSpace - Flag to trim the leading space of the value.
* Default: 'false'
* @property lazyQuotes - Allow unquoted quote in a quoted field or non double
* quoted quotes in quoted field Default: 'false'
* @property fieldsPerRecord - Enabling the check of fields for each row.
* If == 0, first row is used as referal for the number of fields.
*/
export interface ParseOptions {
comma?: string;
comment?: string;
trimLeadingSpace?: boolean;
lazyQuotes?: boolean;
fieldsPerRecord?: number;
}
function chkOptions(opt: ParseOptions): void {
if (!opt.comma) opt.comma = ",";
if (!opt.trimLeadingSpace) opt.trimLeadingSpace = false;
if (
INVALID_RUNE.includes(opt.comma!) ||
INVALID_RUNE.includes(opt.comment!) ||
opt.comma === opt.comment
) {
throw new Error("Invalid Delimiter");
}
}
async function read(
Startline: number,
reader: BufReader,
opt: ParseOptions = { comma: ",", trimLeadingSpace: false }
): Promise<string[] | Deno.EOF> {
const tp = new TextProtoReader(reader);
let line: string;
let result: string[] = [];
const lineIndex = Startline;
const r = await tp.readLine();
if (r === Deno.EOF) return Deno.EOF;
line = r;
// Normalize \r\n to \n on all input lines.
if (
line.length >= 2 &&
line[line.length - 2] === "\r" &&
line[line.length - 1] === "\n"
) {
line = line.substring(0, line.length - 2);
line = line + "\n";
}
const trimmedLine = line.trimLeft();
if (trimmedLine.length === 0) {
return [];
}
// line starting with comment character is ignored
if (opt.comment && trimmedLine[0] === opt.comment) {
return [];
}
result = line.split(opt.comma!);
let quoteError = false;
result = result.map((r): string => {
if (opt.trimLeadingSpace) {
r = r.trimLeft();
}
if (r[0] === '"' && r[r.length - 1] === '"') {
r = r.substring(1, r.length - 1);
} else if (r[0] === '"') {
r = r.substring(1, r.length);
}
if (!opt.lazyQuotes) {
if (r[0] !== '"' && r.indexOf('"') !== -1) {
quoteError = true;
}
}
return r;
});
if (quoteError) {
throw new ParseError(Startline, lineIndex, 'bare " in non-quoted-field');
}
return result;
}
export async function readAll(
reader: BufReader,
opt: ParseOptions = {
comma: ",",
trimLeadingSpace: false,
lazyQuotes: false
}
): Promise<string[][]> {
const result: string[][] = [];
let _nbFields: number;
let lineResult: string[];
let first = true;
let lineIndex = 0;
chkOptions(opt);
for (;;) {
const r = await read(lineIndex, reader, opt);
if (r === Deno.EOF) break;
lineResult = r;
lineIndex++;
// If fieldsPerRecord is 0, Read sets it to
// the number of fields in the first record
if (first) {
first = false;
if (opt.fieldsPerRecord !== undefined) {
if (opt.fieldsPerRecord === 0) {
_nbFields = lineResult.length;
} else {
_nbFields = opt.fieldsPerRecord;
}
}
}
if (lineResult.length > 0) {
if (_nbFields! && _nbFields! !== lineResult.length) {
throw new ParseError(lineIndex, lineIndex, "wrong number of fields");
}
result.push(lineResult);
}
}
return result;
}
/**
* HeaderOption provides the column definition
* and the parse function for each entry of the
* column.
*/
export interface HeaderOption {
name: string;
parse?: (input: string) => unknown;
}
export interface ExtendedParseOptions extends ParseOptions {
header: boolean | string[] | HeaderOption[];
parse?: (input: unknown) => unknown;
}
/**
* Csv parse helper to manipulate data.
* Provides an auto/custom mapper for columns and parse function
* for columns and rows.
* @param input Input to parse. Can be a string or BufReader.
* @param opt options of the parser.
* @param [opt.header=false] HeaderOptions
* @param [opt.parse=null] Parse function for rows.
* Example:
* const r = await parseFile('a,b,c\ne,f,g\n', {
* header: ["this", "is", "sparta"],
* parse: (e: Record<string, unknown>) => {
* return { super: e.this, street: e.is, fighter: e.sparta };
* }
* });
* // output
* [
* { super: "a", street: "b", fighter: "c" },
* { super: "e", street: "f", fighter: "g" }
* ]
*/
export async function parse(
input: string | BufReader,
opt: ExtendedParseOptions = {
header: false
}
): Promise<unknown[]> {
let r: string[][];
if (input instanceof BufReader) {
r = await readAll(input, opt);
} else {
r = await readAll(new BufReader(new StringReader(input)), opt);
}
if (opt.header) {
let headers: HeaderOption[] = [];
let i = 0;
if (Array.isArray(opt.header)) {
if (typeof opt.header[0] !== "string") {
headers = opt.header as HeaderOption[];
} else {
const h = opt.header as string[];
headers = h.map(
(e): HeaderOption => {
return {
name: e
};
}
);
}
} else {
headers = r.shift()!.map(
(e): HeaderOption => {
return {
name: e
};
}
);
i++;
}
return r.map((e): unknown => {
if (e.length !== headers.length) {
throw `Error number of fields line:${i}`;
}
i++;
const out: Record<string, unknown> = {};
for (let j = 0; j < e.length; j++) {
const h = headers[j];
if (h.parse) {
out[h.name] = h.parse(e[j]);
} else {
out[h.name] = e[j];
}
}
if (opt.parse) {
return opt.parse(out);
}
return out;
});
}
if (opt.parse) {
return r.map((e: string[]): unknown => opt.parse!(e));
}
return r;
}

592
std/encoding/csv_test.ts Normal file
View file

@ -0,0 +1,592 @@
// Test ported from Golang
// https://github.com/golang/go/blob/2cc15b1/src/encoding/csv/reader_test.go
import { test, runIfMain } from "../testing/mod.ts";
import { assertEquals, assert } from "../testing/asserts.ts";
import { readAll, parse } from "./csv.ts";
import { StringReader } from "../io/readers.ts";
import { BufReader } from "../io/bufio.ts";
const ErrInvalidDelim = "Invalid Delimiter";
const ErrFieldCount = "wrong number of fields";
const ErrBareQuote = 'bare " in non-quoted-field';
// TODO(zekth): Activate remaining tests
const testCases = [
{
Name: "Simple",
Input: "a,b,c\n",
Output: [["a", "b", "c"]]
},
{
Name: "CRLF",
Input: "a,b\r\nc,d\r\n",
Output: [["a", "b"], ["c", "d"]]
},
{
Name: "BareCR",
Input: "a,b\rc,d\r\n",
Output: [["a", "b\rc", "d"]]
},
// {
// Name: "RFC4180test",
// Input: `#field1,field2,field3
// "aaa","bbb","ccc"
// "a,a","bbb","ccc"
// zzz,yyy,xxx`,
// UseFieldsPerRecord: true,
// FieldsPerRecord: 0,
// Output: [
// ["#field1", "field2", "field3"],
// ["aaa", "bbb", "ccc"],
// ["a,a", `bbb`, "ccc"],
// ["zzz", "yyy", "xxx"]
// ]
// },
{
Name: "NoEOLTest",
Input: "a,b,c",
Output: [["a", "b", "c"]]
},
{
Name: "Semicolon",
Input: "a;b;c\n",
Output: [["a", "b", "c"]],
Comma: ";"
},
// {
// Name: "MultiLine",
// Input: `"two
// line","one line","three
// line
// field"`,
// Output: [["two\nline"], ["one line"], ["three\nline\nfield"]]
// },
{
Name: "BlankLine",
Input: "a,b,c\n\nd,e,f\n\n",
Output: [["a", "b", "c"], ["d", "e", "f"]]
},
{
Name: "BlankLineFieldCount",
Input: "a,b,c\n\nd,e,f\n\n",
Output: [["a", "b", "c"], ["d", "e", "f"]],
UseFieldsPerRecord: true,
FieldsPerRecord: 0
},
{
Name: "TrimSpace",
Input: " a, b, c\n",
Output: [["a", "b", "c"]],
TrimLeadingSpace: true
},
{
Name: "LeadingSpace",
Input: " a, b, c\n",
Output: [[" a", " b", " c"]]
},
{
Name: "Comment",
Input: "#1,2,3\na,b,c\n#comment",
Output: [["a", "b", "c"]],
Comment: "#"
},
{
Name: "NoComment",
Input: "#1,2,3\na,b,c",
Output: [["#1", "2", "3"], ["a", "b", "c"]]
},
{
Name: "LazyQuotes",
Input: `a "word","1"2",a","b`,
Output: [[`a "word"`, `1"2`, `a"`, `b`]],
LazyQuotes: true
},
{
Name: "BareQuotes",
Input: `a "word","1"2",a"`,
Output: [[`a "word"`, `1"2`, `a"`]],
LazyQuotes: true
},
{
Name: "BareDoubleQuotes",
Input: `a""b,c`,
Output: [[`a""b`, `c`]],
LazyQuotes: true
},
{
Name: "BadDoubleQuotes",
Input: `a""b,c`,
Error: ErrBareQuote
// Error: &ParseError{StartLine: 1, Line: 1, Column: 1, Err: ErrBareQuote},
},
{
Name: "TrimQuote",
Input: ` "a"," b",c`,
Output: [["a", " b", "c"]],
TrimLeadingSpace: true
},
{
Name: "BadBareQuote",
Input: `a "word","b"`,
Error: ErrBareQuote
// &ParseError{StartLine: 1, Line: 1, Column: 2, Err: ErrBareQuote}
},
{
Name: "BadTrailingQuote",
Input: `"a word",b"`,
Error: ErrBareQuote
},
{
Name: "ExtraneousQuote",
Input: `"a "word","b"`,
Error: ErrBareQuote
},
{
Name: "BadFieldCount",
Input: "a,b,c\nd,e",
Error: ErrFieldCount,
UseFieldsPerRecord: true,
FieldsPerRecord: 0
},
{
Name: "BadFieldCount1",
Input: `a,b,c`,
// Error: &ParseError{StartLine: 1, Line: 1, Err: ErrFieldCount},
UseFieldsPerRecord: true,
FieldsPerRecord: 2,
Error: ErrFieldCount
},
{
Name: "FieldCount",
Input: "a,b,c\nd,e",
Output: [["a", "b", "c"], ["d", "e"]]
},
{
Name: "TrailingCommaEOF",
Input: "a,b,c,",
Output: [["a", "b", "c", ""]]
},
{
Name: "TrailingCommaEOL",
Input: "a,b,c,\n",
Output: [["a", "b", "c", ""]]
},
{
Name: "TrailingCommaSpaceEOF",
Input: "a,b,c, ",
Output: [["a", "b", "c", ""]],
TrimLeadingSpace: true
},
{
Name: "TrailingCommaSpaceEOL",
Input: "a,b,c, \n",
Output: [["a", "b", "c", ""]],
TrimLeadingSpace: true
},
{
Name: "TrailingCommaLine3",
Input: "a,b,c\nd,e,f\ng,hi,",
Output: [["a", "b", "c"], ["d", "e", "f"], ["g", "hi", ""]],
TrimLeadingSpace: true
},
{
Name: "NotTrailingComma3",
Input: "a,b,c, \n",
Output: [["a", "b", "c", " "]]
},
{
Name: "CommaFieldTest",
Input: `x,y,z,w
x,y,z,
x,y,,
x,,,
,,,
"x","y","z","w"
"x","y","z",""
"x","y","",""
"x","","",""
"","","",""
`,
Output: [
["x", "y", "z", "w"],
["x", "y", "z", ""],
["x", "y", "", ""],
["x", "", "", ""],
["", "", "", ""],
["x", "y", "z", "w"],
["x", "y", "z", ""],
["x", "y", "", ""],
["x", "", "", ""],
["", "", "", ""]
]
},
{
Name: "TrailingCommaIneffective1",
Input: "a,b,\nc,d,e",
Output: [["a", "b", ""], ["c", "d", "e"]],
TrimLeadingSpace: true
},
{
Name: "ReadAllReuseRecord",
Input: "a,b\nc,d",
Output: [["a", "b"], ["c", "d"]],
ReuseRecord: true
},
// {
// Name: "StartLine1", // Issue 19019
// Input: 'a,"b\nc"d,e',
// Error: true
// // Error: &ParseError{StartLine: 1, Line: 2, Column: 1, Err: ErrQuote},
// },
// {
// Name: "StartLine2",
// Input: 'a,b\n"d\n\n,e',
// Error: true
// // Error: &ParseError{StartLine: 2, Line: 5, Column: 0, Err: ErrQuote},
// },
// {
// Name: "CRLFInQuotedField", // Issue 21201
// Input: 'A,"Hello\r\nHi",B\r\n',
// Output: [["A", "Hello\nHi", "B"]]
// },
{
Name: "BinaryBlobField", // Issue 19410
Input: "x09\x41\xb4\x1c,aktau",
Output: [["x09A\xb4\x1c", "aktau"]]
},
// {
// Name: "TrailingCR",
// Input: "field1,field2\r",
// Output: [["field1", "field2"]]
// },
// {
// Name: "QuotedTrailingCR",
// Input: '"field"\r',
// Output: [['"field"']]
// },
// {
// Name: "QuotedTrailingCRCR",
// Input: '"field"\r\r',
// Error: true,
// // Error: &ParseError{StartLine: 1, Line: 1, Column: 6, Err: ErrQuote},
// },
// {
// Name: "FieldCR",
// Input: "field\rfield\r",
// Output: [["field\rfield"]]
// },
// {
// Name: "FieldCRCR",
// Input: "field\r\rfield\r\r",
// Output: [["field\r\rfield\r"]]
// },
{
Name: "FieldCRCRLF",
Input: "field\r\r\nfield\r\r\n",
Output: [["field\r"], ["field\r"]]
},
{
Name: "FieldCRCRLFCR",
Input: "field\r\r\n\rfield\r\r\n\r",
Output: [["field\r"], ["\rfield\r"]]
},
// {
// Name: "FieldCRCRLFCRCR",
// Input: "field\r\r\n\r\rfield\r\r\n\r\r",
// Output: [["field\r"], ["\r\rfield\r"], ["\r"]]
// },
// {
// Name: "MultiFieldCRCRLFCRCR",
// Input: "field1,field2\r\r\n\r\rfield1,field2\r\r\n\r\r,",
// Output: [
// ["field1", "field2\r"],
// ["\r\rfield1", "field2\r"],
// ["\r\r", ""]
// ]
// },
{
Name: "NonASCIICommaAndComment",
Input: "a£b,c£ \td,e\n€ comment\n",
Output: [["a", "b,c", "d,e"]],
TrimLeadingSpace: true,
Comma: "£",
Comment: "€"
},
{
Name: "NonASCIICommaAndCommentWithQuotes",
Input: 'a€" b,"€ c\nλ comment\n',
Output: [["a", " b,", " c"]],
Comma: "€",
Comment: "λ"
},
{
// λ and θ start with the same byte.
// This tests that the parser doesn't confuse such characters.
Name: "NonASCIICommaConfusion",
Input: '"abθcd"λefθgh',
Output: [["abθcd", "efθgh"]],
Comma: "λ",
Comment: "€"
},
{
Name: "NonASCIICommentConfusion",
Input: "λ\nλ\nθ\nλ\n",
Output: [["λ"], ["λ"], ["λ"]],
Comment: "θ"
},
// {
// Name: "QuotedFieldMultipleLF",
// Input: '"\n\n\n\n"',
// Output: [["\n\n\n\n"]]
// },
// {
// Name: "MultipleCRLF",
// Input: "\r\n\r\n\r\n\r\n"
// },
/**
* The implementation may read each line in several chunks if
* it doesn't fit entirely.
* in the read buffer, so we should test the code to handle that condition.
*/
// {
// Name: "HugeLines",
// Input:
// strings.Repeat("#ignore\n", 10000) +
// strings.Repeat("@", 5000) +
// "," +
// strings.Repeat("*", 5000),
// Output: [[strings.Repeat("@", 5000), strings.Repeat("*", 5000)]],
// Comment: "#"
// },
{
Name: "QuoteWithTrailingCRLF",
Input: '"foo"bar"\r\n',
Error: ErrBareQuote
// Error: &ParseError{StartLine: 1, Line: 1, Column: 4, Err: ErrQuote},
},
{
Name: "LazyQuoteWithTrailingCRLF",
Input: '"foo"bar"\r\n',
Output: [[`foo"bar`]],
LazyQuotes: true
},
// {
// Name: "DoubleQuoteWithTrailingCRLF",
// Input: '"foo""bar"\r\n',
// Output: [[`foo"bar`]]
// },
// {
// Name: "EvenQuotes",
// Input: `""""""""`,
// Output: [[`"""`]]
// },
// {
// Name: "OddQuotes",
// Input: `"""""""`,
// Error: true
// // Error:" &ParseError{StartLine: 1, Line: 1, Column: 7, Err: ErrQuote}",
// },
// {
// Name: "LazyOddQuotes",
// Input: `"""""""`,
// Output: [[`"""`]],
// LazyQuotes: true
// },
{
Name: "BadComma1",
Comma: "\n",
Error: ErrInvalidDelim
},
{
Name: "BadComma2",
Comma: "\r",
Error: ErrInvalidDelim
},
{
Name: "BadComma3",
Comma: '"',
Error: ErrInvalidDelim
},
{
Name: "BadComment1",
Comment: "\n",
Error: ErrInvalidDelim
},
{
Name: "BadComment2",
Comment: "\r",
Error: ErrInvalidDelim
},
{
Name: "BadCommaComment",
Comma: "X",
Comment: "X",
Error: ErrInvalidDelim
}
];
for (const t of testCases) {
test({
name: `[CSV] ${t.Name}`,
async fn(): Promise<void> {
let comma = ",";
let comment;
let fieldsPerRec;
let trim = false;
let lazyquote = false;
if (t.Comma) {
comma = t.Comma;
}
if (t.Comment) {
comment = t.Comment;
}
if (t.TrimLeadingSpace) {
trim = true;
}
if (t.UseFieldsPerRecord) {
fieldsPerRec = t.FieldsPerRecord;
}
if (t.LazyQuotes) {
lazyquote = t.LazyQuotes;
}
let actual;
if (t.Error) {
let err;
try {
actual = await readAll(new BufReader(new StringReader(t.Input!)), {
comma: comma,
comment: comment,
trimLeadingSpace: trim,
fieldsPerRecord: fieldsPerRec,
lazyQuotes: lazyquote
});
} catch (e) {
err = e;
}
assert(err);
assertEquals(err.message, t.Error);
} else {
actual = await readAll(new BufReader(new StringReader(t.Input!)), {
comma: comma,
comment: comment,
trimLeadingSpace: trim,
fieldsPerRecord: fieldsPerRec,
lazyQuotes: lazyquote
});
const expected = t.Output;
assertEquals(actual, expected);
}
}
});
}
const parseTestCases = [
{
name: "simple",
in: "a,b,c",
header: false,
result: [["a", "b", "c"]]
},
{
name: "simple Bufreader",
in: new BufReader(new StringReader("a,b,c")),
header: false,
result: [["a", "b", "c"]]
},
{
name: "multiline",
in: "a,b,c\ne,f,g\n",
header: false,
result: [["a", "b", "c"], ["e", "f", "g"]]
},
{
name: "header mapping boolean",
in: "a,b,c\ne,f,g\n",
header: true,
result: [{ a: "e", b: "f", c: "g" }]
},
{
name: "header mapping array",
in: "a,b,c\ne,f,g\n",
header: ["this", "is", "sparta"],
result: [
{ this: "a", is: "b", sparta: "c" },
{ this: "e", is: "f", sparta: "g" }
]
},
{
name: "header mapping object",
in: "a,b,c\ne,f,g\n",
header: [{ name: "this" }, { name: "is" }, { name: "sparta" }],
result: [
{ this: "a", is: "b", sparta: "c" },
{ this: "e", is: "f", sparta: "g" }
]
},
{
name: "header mapping parse entry",
in: "a,b,c\ne,f,g\n",
header: [
{
name: "this",
parse: (e: string): string => {
return `b${e}$$`;
}
},
{
name: "is",
parse: (e: string): number => {
return e.length;
}
},
{
name: "sparta",
parse: (e: string): unknown => {
return { bim: `boom-${e}` };
}
}
],
result: [
{ this: "ba$$", is: 1, sparta: { bim: `boom-c` } },
{ this: "be$$", is: 1, sparta: { bim: `boom-g` } }
]
},
{
name: "multiline parse",
in: "a,b,c\ne,f,g\n",
parse: (e: string[]): unknown => {
return { super: e[0], street: e[1], fighter: e[2] };
},
header: false,
result: [
{ super: "a", street: "b", fighter: "c" },
{ super: "e", street: "f", fighter: "g" }
]
},
{
name: "header mapping object parseline",
in: "a,b,c\ne,f,g\n",
header: [{ name: "this" }, { name: "is" }, { name: "sparta" }],
parse: (e: Record<string, unknown>): unknown => {
return { super: e.this, street: e.is, fighter: e.sparta };
},
result: [
{ super: "a", street: "b", fighter: "c" },
{ super: "e", street: "f", fighter: "g" }
]
}
];
for (const testCase of parseTestCases) {
test({
name: `[CSV] Parse ${testCase.name}`,
async fn(): Promise<void> {
const r = await parse(testCase.in, {
header: testCase.header,
parse: testCase.parse as (input: unknown) => unknown
});
assertEquals(r, testCase.result);
}
});
}
runIfMain(import.meta);

143
std/encoding/hex.ts Normal file
View file

@ -0,0 +1,143 @@
// Ported from Go
// https://github.com/golang/go/blob/go1.12.5/src/encoding/hex/hex.go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
const hextable = new TextEncoder().encode("0123456789abcdef");
export function errInvalidByte(byte: number): Error {
return new Error(
"encoding/hex: invalid byte: " +
new TextDecoder().decode(new Uint8Array([byte]))
);
}
export function errLength(): Error {
return new Error("encoding/hex: odd length hex string");
}
// fromHexChar converts a hex character into its value and a success flag.
function fromHexChar(byte: number): [number, boolean] {
switch (true) {
case 48 <= byte && byte <= 57: // '0' <= byte && byte <= '9'
return [byte - 48, true];
case 97 <= byte && byte <= 102: // 'a' <= byte && byte <= 'f'
return [byte - 97 + 10, true];
case 65 <= byte && byte <= 70: // 'A' <= byte && byte <= 'F'
return [byte - 65 + 10, true];
}
return [0, false];
}
/**
* EncodedLen returns the length of an encoding of n source bytes. Specifically,
* it returns n * 2.
* @param n
*/
export function encodedLen(n: number): number {
return n * 2;
}
/**
* Encode encodes `src` into `encodedLen(src.length)` bytes of `dst`.
* As a convenience, it returns the number of bytes written to `dst`
* but this value is always `encodedLen(src.length)`.
* Encode implements hexadecimal encoding.
* @param dst
* @param src
*/
export function encode(dst: Uint8Array, src: Uint8Array): number {
const srcLength = encodedLen(src.length);
if (dst.length !== srcLength) {
throw new Error("Out of index.");
}
for (let i = 0; i < src.length; i++) {
const v = src[i];
dst[i * 2] = hextable[v >> 4];
dst[i * 2 + 1] = hextable[v & 0x0f];
}
return srcLength;
}
/**
* EncodeToString returns the hexadecimal encoding of `src`.
* @param src
*/
export function encodeToString(src: Uint8Array): string {
const dest = new Uint8Array(encodedLen(src.length));
encode(dest, src);
return new TextDecoder().decode(dest);
}
/**
* Decode decodes `src` into `decodedLen(src.length)` bytes
* returning the actual number of bytes written to `dst`.
* Decode expects that `src` contains only hexadecimal characters and that `src`
* has even length.
* If the input is malformed, Decode returns the number of bytes decoded before
* the error.
* @param dst
* @param src
*/
export function decode(
dst: Uint8Array,
src: Uint8Array
): [number, Error | void] {
let i = 0;
for (; i < Math.floor(src.length / 2); i++) {
const [a, aOK] = fromHexChar(src[i * 2]);
if (!aOK) {
return [i, errInvalidByte(src[i * 2])];
}
const [b, bOK] = fromHexChar(src[i * 2 + 1]);
if (!bOK) {
return [i, errInvalidByte(src[i * 2 + 1])];
}
dst[i] = (a << 4) | b;
}
if (src.length % 2 == 1) {
// Check for invalid char before reporting bad length,
// since the invalid char (if present) is an earlier problem.
const [, ok] = fromHexChar(src[i * 2]);
if (!ok) {
return [i, errInvalidByte(src[i * 2])];
}
return [i, errLength()];
}
return [i, undefined];
}
/**
* DecodedLen returns the length of a decoding of `x` source bytes.
* Specifically, it returns `x / 2`.
* @param x
*/
export function decodedLen(x: number): number {
return Math.floor(x / 2);
}
/**
* DecodeString returns the bytes represented by the hexadecimal string `s`.
* DecodeString expects that src contains only hexadecimal characters and that
* src has even length.
* If the input is malformed, DecodeString will throws an error.
* @param s the `string` need to decode to `Uint8Array`
*/
export function decodeString(s: string): Uint8Array {
const src = new TextEncoder().encode(s);
// We can use the source slice itself as the destination
// because the decode loop increments by one and then the 'seen' byte is not
// used anymore.
const [n, err] = decode(src, src);
if (err) {
throw err;
}
return src.slice(0, n);
}

182
std/encoding/hex_test.ts Normal file
View file

@ -0,0 +1,182 @@
// Ported from Go
// https://github.com/golang/go/blob/go1.12.5/src/encoding/hex/hex.go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test, runIfMain } from "../testing/mod.ts";
import { assertEquals, assertThrows } from "../testing/asserts.ts";
import {
encodedLen,
encode,
encodeToString,
decodedLen,
decode,
decodeString,
errLength,
errInvalidByte
} from "./hex.ts";
function toByte(s: string): number {
return new TextEncoder().encode(s)[0];
}
const testCases = [
// encoded(hex) / decoded(Uint8Array)
["", []],
["0001020304050607", [0, 1, 2, 3, 4, 5, 6, 7]],
["08090a0b0c0d0e0f", [8, 9, 10, 11, 12, 13, 14, 15]],
["f0f1f2f3f4f5f6f7", [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]],
["f8f9fafbfcfdfeff", [0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff]],
["67", Array.from(new TextEncoder().encode("g"))],
["e3a1", [0xe3, 0xa1]]
];
const errCases = [
// encoded(hex) / error
["", "", undefined],
["0", "", errLength()],
["zd4aa", "", errInvalidByte(toByte("z"))],
["d4aaz", "\xd4\xaa", errInvalidByte(toByte("z"))],
["30313", "01", errLength()],
["0g", "", errInvalidByte(new TextEncoder().encode("g")[0])],
["00gg", "\x00", errInvalidByte(new TextEncoder().encode("g")[0])],
["0\x01", "", errInvalidByte(new TextEncoder().encode("\x01")[0])],
["ffeed", "\xff\xee", errLength()]
];
test({
name: "[encoding.hex] encodedLen",
fn(): void {
assertEquals(encodedLen(0), 0);
assertEquals(encodedLen(1), 2);
assertEquals(encodedLen(2), 4);
assertEquals(encodedLen(3), 6);
assertEquals(encodedLen(4), 8);
}
});
test({
name: "[encoding.hex] encode",
fn(): void {
{
const srcStr = "abc";
const src = new TextEncoder().encode(srcStr);
const dest = new Uint8Array(encodedLen(src.length));
const int = encode(dest, src);
assertEquals(src, new Uint8Array([97, 98, 99]));
assertEquals(int, 6);
}
{
const srcStr = "abc";
const src = new TextEncoder().encode(srcStr);
const dest = new Uint8Array(2); // out of index
assertThrows(
(): void => {
encode(dest, src);
},
Error,
"Out of index."
);
}
for (const [enc, dec] of testCases) {
const dest = new Uint8Array(encodedLen(dec.length));
const src = new Uint8Array(dec as number[]);
const n = encode(dest, src);
assertEquals(dest.length, n);
assertEquals(new TextDecoder().decode(dest), enc);
}
}
});
test({
name: "[encoding.hex] encodeToString",
fn(): void {
for (const [enc, dec] of testCases) {
assertEquals(encodeToString(new Uint8Array(dec as number[])), enc);
}
}
});
test({
name: "[encoding.hex] decodedLen",
fn(): void {
assertEquals(decodedLen(0), 0);
assertEquals(decodedLen(2), 1);
assertEquals(decodedLen(4), 2);
assertEquals(decodedLen(6), 3);
assertEquals(decodedLen(8), 4);
}
});
test({
name: "[encoding.hex] decode",
fn(): void {
// Case for decoding uppercase hex characters, since
// Encode always uses lowercase.
const extraTestcase = [
["F8F9FAFBFCFDFEFF", [0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff]]
];
const cases = testCases.concat(extraTestcase);
for (const [enc, dec] of cases) {
const dest = new Uint8Array(decodedLen(enc.length));
const src = new TextEncoder().encode(enc as string);
const [, err] = decode(dest, src);
assertEquals(err, undefined);
assertEquals(Array.from(dest), Array.from(dec as number[]));
}
}
});
test({
name: "[encoding.hex] decodeString",
fn(): void {
for (const [enc, dec] of testCases) {
const dst = decodeString(enc as string);
assertEquals(dec, Array.from(dst));
}
}
});
test({
name: "[encoding.hex] decode error",
fn(): void {
for (const [input, output, expectedErr] of errCases) {
const out = new Uint8Array((input as string).length + 10);
const [n, err] = decode(out, new TextEncoder().encode(input as string));
assertEquals(
new TextDecoder("ascii").decode(out.slice(0, n)),
output as string
);
assertEquals(err, expectedErr);
}
}
});
test({
name: "[encoding.hex] decodeString error",
fn(): void {
for (const [input, output, expectedErr] of errCases) {
if (expectedErr) {
assertThrows(
(): void => {
decodeString(input as string);
},
Error,
(expectedErr as Error).message
);
} else {
const out = decodeString(input as string);
assertEquals(new TextDecoder("ascii").decode(out), output as string);
}
}
}
});
runIfMain(import.meta);

3
std/encoding/testdata/CRLF.toml vendored Normal file
View file

@ -0,0 +1,3 @@
[boolean]
bool1 = true
bool2 = false

12
std/encoding/testdata/arrayTable.toml vendored Normal file
View file

@ -0,0 +1,12 @@
[[bin]]
name = "deno"
path = "cli/main.rs"
[[bin]]
name = "deno_core"
path = "src/foo.rs"
[[nib]]
name = "node"
path = "not_found"

8
std/encoding/testdata/arrays.toml vendored Normal file
View file

@ -0,0 +1,8 @@
[arrays]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]

3
std/encoding/testdata/boolean.toml vendored Normal file
View file

@ -0,0 +1,3 @@
[boolean] # i hate comments
bool1 = true
bool2 = false

56
std/encoding/testdata/cargo.toml vendored Normal file
View file

@ -0,0 +1,56 @@
# Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
# Dummy package info required by `cargo fetch`.
# Use tools/sync_third_party.py to install deps after editing this file.
# Deno does not build with cargo. Deno uses a build system called gn.
# See build_extra/rust/BUILD.gn for the manually built configuration of rust
# crates.
[workspace]
members = [
"./",
"core",
]
[[bin]]
name = "deno"
path = "cli/main.rs"
[package]
name = "deno"
version = "0.3.4"
edition = "2018"
[dependencies]
deno_core = { path = "./core" }
ansi_term = "0.11.0"
atty = "0.2.11"
dirs = "1.0.5"
flatbuffers = "0.5.0"
futures = "0.1.25"
getopts = "0.2.18"
http = "0.1.16"
hyper = "0.12.24"
hyper-rustls = "0.16.0"
integer-atomics = "1.0.2"
lazy_static = "1.3.0"
libc = "0.2.49"
log = "0.4.6"
rand = "0.6.5"
regex = "1.1.0"
remove_dir_all = "0.5.1"
ring = "0.14.6"
rustyline = "3.0.0"
serde_json = "1.0.38"
source-map-mappings = "0.5.0"
tempfile = "3.0.7"
tokio = "0.1.15"
tokio-executor = "0.1.6"
tokio-fs = "0.1.5"
tokio-io = "0.1.11"
tokio-process = "0.2.3"
tokio-threadpool = "0.1.11"
url = "1.7.2"
[target.'cfg(windows)'.dependencies]
winapi = "0.3.6"

147
std/encoding/testdata/cargoTest.toml vendored Normal file
View file

@ -0,0 +1,147 @@
# This is a TOML document.
title = "TOML Example"
[deeply.nested.object.in.the.toml]
name = "Tom Preston-Werner"
dob = 2009-05-27T07:32:00
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
[strings]
str0 = "deno"
str1 = """
Roses are red
Violets are blue"""
# On a Unix system, the above multi-line string will most likely be the same as:
str2 = "Roses are red\nViolets are blue"
# On a Windows system, it will most likely be equivalent to:
str3 = "Roses are red\r\nViolets are blue"
str4 = "The quick brown fox jumps over the lazy dog."
str5 = "this is a \"quote\""
str5 = """
The quick brown \
fox jumps over \
the lazy dog."""
str6 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
[Integer]
int1 = +99
int2 = 42
int3 = 0
int4 = -17
int5 = 1_000
int6 = 5_349_221
int7 = 1_2_3_4_5 # VALID but discouraged
# hexadecimal with prefix `0x`
hex1 = 0xDEADBEEF
hex2 = 0xdeadbeef
hex3 = 0xdead_beef
# octal with prefix `0o`
oct1 = 0o01234567
oct2 = 0o755 # useful for Unix file permissions
# binary with prefix `0b`
bin1 = 0b11010110
[Date-Time]
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
odt4 = 1979-05-27 07:32:00Z
ld1 = 1979-05-27
lt1 = 07:32:00 #buggy
lt2 = 00:32:00.999999 #buggy
[boolean]
bool1 = true
bool2 = false
[float]
# fractional
flt1 = +1.0
flt2 = 3.1415
flt3 = -0.01
# exponent
flt4 = 5e+22
flt5 = 1e6
flt6 = -2E-2
# both
flt7 = 6.626e-34
flt8 = 224_617.445_991_228
# infinity
sf1 = inf # positive infinity
sf2 = +inf # positive infinity
sf3 = -inf # negative infinity
# not a number
sf4 = nan # actual sNaN/qNaN encoding is implementation specific
sf5 = +nan # same as `nan`
sf6 = -nan # valid, actual encoding is implementation specific
[Table]
name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
animal = { type.name = "pug" }
[[fruit]]
name = "apple"
[fruit.physical]
color = "red"
shape = "round"
[[fruit.variety]]
name = "red delicious"
[[fruit.variety]]
name = "granny smith"
[[fruit]]
name = "banana"
[[fruit.variety]]
name = "plantain"

8
std/encoding/testdata/datetime.toml vendored Normal file
View file

@ -0,0 +1,8 @@
[datetime]
odt1 = 1979-05-27T07:32:00Z # Comment
odt2 = 1979-05-27T00:32:00-07:00 # Comment
odt3 = 1979-05-27T00:32:00.999999-07:00 # Comment
odt4 = 1979-05-27 07:32:00Z # Comment
ld1 = 1979-05-27 # Comment
lt1 = 07:32:00 # Comment
lt2 = 00:32:00.999999 # Comment

23
std/encoding/testdata/float.toml vendored Normal file
View file

@ -0,0 +1,23 @@
[float]
# fractional
flt1 = +1.0 # Comment
flt2 = 3.1415 # Comment
flt3 = -0.01 # Comment
# exponent
flt4 = 5e+22 # Comment
flt5 = 1e6 # Comment
flt6 = -2E-2 # Comment
# both
flt7 = 6.626e-34 # Comment
flt8 = 224_617.445_991_228 # Comment
# infinity
sf1 = inf # positive infinity
sf2 = +inf # positive infinity
sf3 = -inf # negative infinity
# not a number
sf4 = nan # actual sNaN/qNaN encoding is implementation specific
sf5 = +nan # same as `nan`
sf6 = -nan # valid, actual encoding is implementation specific

View file

@ -0,0 +1,7 @@
[inlinetable]
name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
dog = { type = { name = "pug" } }
animal.as.leaders = "tosin"
"tosin.abasi" = "guitarist"
nile = { derek.roddy = "drummer", also = { malevolant.creation = { drum.kit = "Tama" } } }

20
std/encoding/testdata/integer.toml vendored Normal file
View file

@ -0,0 +1,20 @@
[integer]
int1 = +99
int2 = 42
int3 = 0
int4 = -17
int5 = 1_000
int6 = 5_349_221
int7 = 1_2_3_4_5 # VALID but discouraged
# hexadecimal with prefix `0x`
hex1 = 0xDEADBEEF
hex2 = 0xdeadbeef
hex3 = 0xdead_beef
# octal with prefix `0o`
oct1 = 0o01234567
oct2 = 0o755 # useful for Unix file permissions
# binary with prefix `0b`
bin1 = 0b11010110

5
std/encoding/testdata/simple.toml vendored Normal file
View file

@ -0,0 +1,5 @@
deno = "is"
not = "[node]"
regex = '<\i\c*\s*>'
NANI = '何?!'
comment = "Comment inside # the comment" # Comment

30
std/encoding/testdata/string.toml vendored Normal file
View file

@ -0,0 +1,30 @@
[strings]
str0 = "deno"
str1 = """
Roses are not Deno
Violets are not Deno either"""
# On a Unix system, the above multi-line string will most likely be the same as:
str2 = "Roses are not Deno\nViolets are not Deno either"
# On a Windows system, it will most likely be equivalent to:
str3 = "Roses are not Deno\r\nViolets are not Deno either"
str4 = "this is a \"quote\""
str5 = """
The quick brown \
fox jumps over \
the lazy dog."""
str6 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''

13
std/encoding/testdata/table.toml vendored Normal file
View file

@ -0,0 +1,13 @@
[deeply.nested.object.in.the.toml]
name = "Tom Preston-Werner"
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc20"

565
std/encoding/toml.ts Normal file
View file

@ -0,0 +1,565 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { deepAssign } from "../util/deep_assign.ts";
import { pad } from "../strings/pad.ts";
class KeyValuePair {
constructor(public key: string, public value: unknown) {}
}
class ParserGroup {
arrValues: unknown[] = [];
objValues: Record<string, unknown> = {};
constructor(public type: string, public name: string) {}
}
class ParserContext {
currentGroup?: ParserGroup;
output: Record<string, unknown> = {};
}
class Parser {
tomlLines: string[];
context: ParserContext;
constructor(tomlString: string) {
this.tomlLines = this._split(tomlString);
this.context = new ParserContext();
}
_sanitize(): void {
const out: string[] = [];
for (let i = 0; i < this.tomlLines.length; i++) {
const s = this.tomlLines[i];
const trimmed = s.trim();
if (trimmed !== "" && trimmed[0] !== "#") {
out.push(s);
}
}
this.tomlLines = out;
this._mergeMultilines();
}
_mergeMultilines(): void {
function arrayStart(line: string): boolean {
const reg = /.*=\s*\[/g;
return reg.test(line) && !(line[line.length - 1] === "]");
}
function arrayEnd(line: string): boolean {
return line[line.length - 1] === "]";
}
function stringStart(line: string): boolean {
const m = line.match(/.*=\s*(?:\"\"\"|''')/);
if (!m) {
return false;
}
return !line.endsWith(`"""`) || !line.endsWith(`'''`);
}
function stringEnd(line: string): boolean {
return line.endsWith(`'''`) || line.endsWith(`"""`);
}
function isLiteralString(line: string): boolean {
return line.match(/'''/) ? true : false;
}
const merged = [];
let acc = [],
isLiteral = false,
capture = false,
captureType = "",
merge = false;
for (let i = 0; i < this.tomlLines.length; i++) {
const line = this.tomlLines[i];
const trimmed = line.trim();
if (!capture && arrayStart(trimmed)) {
capture = true;
captureType = "array";
} else if (!capture && stringStart(trimmed)) {
isLiteral = isLiteralString(trimmed);
capture = true;
captureType = "string";
} else if (capture && arrayEnd(trimmed)) {
merge = true;
} else if (capture && stringEnd(trimmed)) {
merge = true;
}
if (capture) {
if (isLiteral) {
acc.push(line);
} else {
acc.push(trimmed);
}
} else {
if (isLiteral) {
merged.push(line);
} else {
merged.push(trimmed);
}
}
if (merge) {
capture = false;
merge = false;
if (captureType === "string") {
merged.push(
acc
.join("\n")
.replace(/"""/g, '"')
.replace(/'''/g, `'`)
.replace(/\n/g, "\\n")
);
isLiteral = false;
} else {
merged.push(acc.join(""));
}
captureType = "";
acc = [];
}
}
this.tomlLines = merged;
}
_unflat(keys: string[], values: object = {}, cObj: object = {}): object {
const out: Record<string, unknown> = {};
if (keys.length === 0) {
return cObj;
} else {
if (Object.keys(cObj).length === 0) {
cObj = values;
}
const key: string | undefined = keys.pop();
if (key) {
out[key] = cObj;
}
return this._unflat(keys, values, out);
}
}
_groupToOutput(): void {
const arrProperty = this.context
.currentGroup!.name.replace(/"/g, "")
.replace(/'/g, "")
.split(".");
let u = {};
if (this.context.currentGroup!.type === "array") {
u = this._unflat(arrProperty, this.context.currentGroup!.arrValues);
} else {
u = this._unflat(arrProperty, this.context.currentGroup!.objValues);
}
deepAssign(this.context.output, u);
delete this.context.currentGroup;
}
_split(str: string): string[] {
const out = [];
out.push(...str.split("\n"));
return out;
}
_isGroup(line: string): boolean {
const t = line.trim();
return t[0] === "[" && /\[(.*)\]/.exec(t) ? true : false;
}
_isDeclaration(line: string): boolean {
return line.split("=").length > 1;
}
_createGroup(line: string): void {
const captureReg = /\[(.*)\]/;
if (this.context.currentGroup) {
this._groupToOutput();
}
let type;
let name = line.match(captureReg)![1];
if (name.match(/\[.*\]/)) {
type = "array";
name = name.match(captureReg)![1];
} else {
type = "object";
}
this.context.currentGroup = new ParserGroup(type, name);
}
_processDeclaration(line: string): KeyValuePair {
const idx = line.indexOf("=");
const key = line.substring(0, idx).trim();
const value = this._parseData(line.slice(idx + 1));
return new KeyValuePair(key, value);
}
// TODO (zekth) Need refactor using ACC
_parseData(dataString: string): unknown {
dataString = dataString.trim();
if (this._isDate(dataString)) {
return new Date(dataString.split("#")[0].trim());
}
if (this._isLocalTime(dataString)) {
return eval(`"${dataString.split("#")[0].trim()}"`);
}
const cut3 = dataString.substring(0, 3).toLowerCase();
const cut4 = dataString.substring(0, 4).toLowerCase();
if (cut3 === "inf" || cut4 === "+inf") {
return Infinity;
}
if (cut4 === "-inf") {
return -Infinity;
}
if (cut3 === "nan" || cut4 === "+nan" || cut4 === "-nan") {
return NaN;
}
// If binary / octal / hex
const hex = /(0(?:x|o|b)[0-9a-f_]*)[^#]/gi.exec(dataString);
if (hex && hex[0]) {
return hex[0].trim();
}
const testNumber = this._isParsableNumber(dataString);
if (testNumber && !isNaN(testNumber as number)) {
return testNumber;
}
const invalidArr = /,\]/g.exec(dataString);
if (invalidArr) {
dataString = dataString.replace(/,]/g, "]");
}
const m = /(?:\'|\[|{|\").*(?:\'|\]|\"|})\s*[^#]/g.exec(dataString);
if (m) {
dataString = m[0].trim();
}
if (dataString[0] === "{" && dataString[dataString.length - 1] === "}") {
const reg = /([a-zA-Z0-9-_\.]*) (=)/gi;
let result;
while ((result = reg.exec(dataString))) {
const ogVal = result[0];
const newVal = ogVal
.replace(result[1], `"${result[1]}"`)
.replace(result[2], ":");
dataString = dataString.replace(ogVal, newVal);
}
return JSON.parse(dataString);
}
// Handle First and last EOL for multiline strings
if (dataString.startsWith(`"\\n`)) {
dataString = dataString.replace(`"\\n`, `"`);
} else if (dataString.startsWith(`'\\n`)) {
dataString = dataString.replace(`'\\n`, `'`);
}
if (dataString.endsWith(`\\n"`)) {
dataString = dataString.replace(`\\n"`, `"`);
} else if (dataString.endsWith(`\\n'`)) {
dataString = dataString.replace(`\\n'`, `'`);
}
return eval(dataString);
}
_isLocalTime(str: string): boolean {
const reg = /(\d{2}):(\d{2}):(\d{2})/;
return reg.test(str);
}
_isParsableNumber(dataString: string): number | boolean {
const m = /((?:\+|-|)[0-9_\.e+\-]*)[^#]/i.exec(dataString.trim());
if (!m) {
return false;
} else {
return parseFloat(m[0].replace(/_/g, ""));
}
}
_isDate(dateStr: string): boolean {
const reg = /\d{4}-\d{2}-\d{2}/;
return reg.test(dateStr);
}
_parseDeclarationName(declaration: string): string[] {
const out = [];
let acc = [];
let inLiteral = false;
for (let i = 0; i < declaration.length; i++) {
const c = declaration[i];
switch (c) {
case ".":
if (!inLiteral) {
out.push(acc.join(""));
acc = [];
} else {
acc.push(c);
}
break;
case `"`:
if (inLiteral) {
inLiteral = false;
} else {
inLiteral = true;
}
break;
default:
acc.push(c);
break;
}
}
if (acc.length !== 0) {
out.push(acc.join(""));
}
return out;
}
_parseLines(): void {
for (let i = 0; i < this.tomlLines.length; i++) {
const line = this.tomlLines[i];
// TODO (zekth) Handle unflat of array of tables
if (this._isGroup(line)) {
// if the current group is an array we push the
// parsed objects in it.
if (
this.context.currentGroup &&
this.context.currentGroup.type === "array"
) {
this.context.currentGroup.arrValues.push(
this.context.currentGroup.objValues
);
this.context.currentGroup.objValues = {};
}
// If we need to create a group or to change group
if (
!this.context.currentGroup ||
(this.context.currentGroup &&
this.context.currentGroup.name !==
line.replace(/\[/g, "").replace(/\]/g, ""))
) {
this._createGroup(line);
continue;
}
}
if (this._isDeclaration(line)) {
const kv = this._processDeclaration(line);
const key = kv.key;
const value = kv.value;
if (!this.context.currentGroup) {
this.context.output[key] = value;
} else {
this.context.currentGroup.objValues[key] = value;
}
}
}
if (this.context.currentGroup) {
if (this.context.currentGroup.type === "array") {
this.context.currentGroup.arrValues.push(
this.context.currentGroup.objValues
);
}
this._groupToOutput();
}
}
_cleanOutput(): void {
this._propertyClean(this.context.output);
}
_propertyClean(obj: Record<string, unknown>): void {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
if (k) {
let v = obj[k];
const pathDeclaration = this._parseDeclarationName(k);
delete obj[k];
if (pathDeclaration.length > 1) {
const shift = pathDeclaration.shift();
if (shift) {
k = shift.replace(/"/g, "");
v = this._unflat(pathDeclaration, v as object);
}
} else {
k = k.replace(/"/g, "");
}
obj[k] = v;
if (v instanceof Object) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._propertyClean(v as any);
}
}
}
}
parse(): object {
this._sanitize();
this._parseLines();
this._cleanOutput();
return this.context.output;
}
}
// Bare keys may only contain ASCII letters,
// ASCII digits, underscores, and dashes (A-Za-z0-9_-).
function joinKeys(keys: string[]): string {
// Dotted keys are a sequence of bare or quoted keys joined with a dot.
// This allows for grouping similar properties together:
return keys
.map((str: string): string => {
return str.match(/[^A-Za-z0-9_-]/) ? `"${str}"` : str;
})
.join(".");
}
class Dumper {
maxPad = 0;
srcObject: object;
output: string[] = [];
constructor(srcObjc: object) {
this.srcObject = srcObjc;
}
dump(): string[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.output = this._parse(this.srcObject as any);
this.output = this._format();
return this.output;
}
_parse(obj: Record<string, unknown>, keys: string[] = []): string[] {
const out = [];
const props = Object.keys(obj);
const propObj = props.filter((e: string): boolean => {
if (obj[e] instanceof Array) {
const d: unknown[] = obj[e] as unknown[];
return !this._isSimplySerializable(d[0]);
}
return !this._isSimplySerializable(obj[e]);
});
const propPrim = props.filter((e: string): boolean => {
if (obj[e] instanceof Array) {
const d: unknown[] = obj[e] as unknown[];
return this._isSimplySerializable(d[0]);
}
return this._isSimplySerializable(obj[e]);
});
const k = propPrim.concat(propObj);
for (let i = 0; i < k.length; i++) {
const prop = k[i];
const value = obj[prop];
if (value instanceof Date) {
out.push(this._dateDeclaration([prop], value));
} else if (typeof value === "string" || value instanceof RegExp) {
out.push(this._strDeclaration([prop], value.toString()));
} else if (typeof value === "number") {
out.push(this._numberDeclaration([prop], value));
} else if (
value instanceof Array &&
this._isSimplySerializable(value[0])
) {
// only if primitives types in the array
out.push(this._arrayDeclaration([prop], value));
} else if (
value instanceof Array &&
!this._isSimplySerializable(value[0])
) {
// array of objects
for (let i = 0; i < value.length; i++) {
out.push("");
out.push(this._headerGroup([...keys, prop]));
out.push(...this._parse(value[i], [...keys, prop]));
}
} else if (typeof value === "object") {
out.push("");
out.push(this._header([...keys, prop]));
if (value) {
const toParse = value as Record<string, unknown>;
out.push(...this._parse(toParse, [...keys, prop]));
}
// out.push(...this._parse(value, `${path}${prop}.`));
}
}
out.push("");
return out;
}
_isSimplySerializable(value: unknown): boolean {
return (
typeof value === "string" ||
typeof value === "number" ||
value instanceof RegExp ||
value instanceof Date ||
value instanceof Array
);
}
_header(keys: string[]): string {
return `[${joinKeys(keys)}]`;
}
_headerGroup(keys: string[]): string {
return `[[${joinKeys(keys)}]]`;
}
_declaration(keys: string[]): string {
const title = joinKeys(keys);
if (title.length > this.maxPad) {
this.maxPad = title.length;
}
return `${title} = `;
}
_arrayDeclaration(keys: string[], value: unknown[]): string {
return `${this._declaration(keys)}${JSON.stringify(value)}`;
}
_strDeclaration(keys: string[], value: string): string {
return `${this._declaration(keys)}"${value}"`;
}
_numberDeclaration(keys: string[], value: number): string {
switch (value) {
case Infinity:
return `${this._declaration(keys)}inf`;
case -Infinity:
return `${this._declaration(keys)}-inf`;
default:
return `${this._declaration(keys)}${value}`;
}
}
_dateDeclaration(keys: string[], value: Date): string {
function dtPad(v: string, lPad = 2): string {
return pad(v, lPad, { char: "0" });
}
const m = dtPad((value.getUTCMonth() + 1).toString());
const d = dtPad(value.getUTCDate().toString());
const h = dtPad(value.getUTCHours().toString());
const min = dtPad(value.getUTCMinutes().toString());
const s = dtPad(value.getUTCSeconds().toString());
const ms = dtPad(value.getUTCMilliseconds().toString(), 3);
// formated date
const fData = `${value.getUTCFullYear()}-${m}-${d}T${h}:${min}:${s}.${ms}`;
return `${this._declaration(keys)}${fData}`;
}
_format(): string[] {
const rDeclaration = /(.*)\s=/;
const out = [];
for (let i = 0; i < this.output.length; i++) {
const l = this.output[i];
// we keep empty entry for array of objects
if (l[0] === "[" && l[1] !== "[") {
// empty object
if (this.output[i + 1] === "") {
i += 1;
continue;
}
out.push(l);
} else {
const m = rDeclaration.exec(l);
if (m) {
out.push(l.replace(m[1], pad(m[1], this.maxPad, { side: "right" })));
} else {
out.push(l);
}
}
}
// Cleaning multiple spaces
const cleanedOutput = [];
for (let i = 0; i < out.length; i++) {
const l = out[i];
if (!(l === "" && out[i + 1] === "")) {
cleanedOutput.push(l);
}
}
return cleanedOutput;
}
}
export function stringify(srcObj: object): string {
return new Dumper(srcObj).dump().join("\n");
}
export function parse(tomlString: string): object {
// File is potentially using EOL CRLF
tomlString = tomlString.replace(/\r\n/g, "\n").replace(/\\\n/g, "\n");
return new Parser(tomlString).parse();
}

410
std/encoding/toml_test.ts Normal file
View file

@ -0,0 +1,410 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { runIfMain, test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { existsSync } from "../fs/exists.ts";
import { readFileStrSync } from "../fs/read_file_str.ts";
import { parse, stringify } from "./toml.ts";
import * as path from "../fs/path/mod.ts";
const testFilesDir = path.resolve("encoding", "testdata");
function parseFile(filePath: string): object {
if (!existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const strFile = readFileStrSync(filePath);
return parse(strFile);
}
test({
name: "[TOML] Strings",
fn(): void {
const expected = {
strings: {
str0: "deno",
str1: "Roses are not Deno\nViolets are not Deno either",
str2: "Roses are not Deno\nViolets are not Deno either",
str3: "Roses are not Deno\r\nViolets are not Deno either",
str4: 'this is a "quote"',
str5: "The quick brown\nfox jumps over\nthe lazy dog.",
str6: "The quick brown\nfox jumps over\nthe lazy dog.",
lines:
"The first newline is\ntrimmed in raw strings.\n All other " +
"whitespace\n is preserved."
}
};
const actual = parseFile(path.join(testFilesDir, "string.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] CRLF",
fn(): void {
const expected = { boolean: { bool1: true, bool2: false } };
const actual = parseFile(path.join(testFilesDir, "CRLF.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Boolean",
fn(): void {
const expected = { boolean: { bool1: true, bool2: false } };
const actual = parseFile(path.join(testFilesDir, "boolean.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Integer",
fn(): void {
const expected = {
integer: {
int1: 99,
int2: 42,
int3: 0,
int4: -17,
int5: 1000,
int6: 5349221,
int7: 12345,
hex1: "0xDEADBEEF",
hex2: "0xdeadbeef",
hex3: "0xdead_beef",
oct1: "0o01234567",
oct2: "0o755",
bin1: "0b11010110"
}
};
const actual = parseFile(path.join(testFilesDir, "integer.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Float",
fn(): void {
const expected = {
float: {
flt1: 1.0,
flt2: 3.1415,
flt3: -0.01,
flt4: 5e22,
flt5: 1e6,
flt6: -2e-2,
flt7: 6.626e-34,
flt8: 224_617.445_991_228,
sf1: Infinity,
sf2: Infinity,
sf3: -Infinity,
sf4: NaN,
sf5: NaN,
sf6: NaN
}
};
const actual = parseFile(path.join(testFilesDir, "float.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Arrays",
fn(): void {
const expected = {
arrays: {
data: [["gamma", "delta"], [1, 2]],
hosts: ["alpha", "omega"]
}
};
const actual = parseFile(path.join(testFilesDir, "arrays.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Table",
fn(): void {
const expected = {
deeply: {
nested: {
object: {
in: {
the: {
toml: {
name: "Tom Preston-Werner"
}
}
}
}
}
},
servers: {
alpha: {
ip: "10.0.0.1",
dc: "eqdc10"
},
beta: {
ip: "10.0.0.2",
dc: "eqdc20"
}
}
};
const actual = parseFile(path.join(testFilesDir, "table.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Simple",
fn(): void {
const expected = {
deno: "is",
not: "[node]",
regex: "<ic*s*>",
NANI: "何?!",
comment: "Comment inside # the comment"
};
const actual = parseFile(path.join(testFilesDir, "simple.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Datetime",
fn(): void {
const expected = {
datetime: {
odt1: new Date("1979-05-27T07:32:00Z"),
odt2: new Date("1979-05-27T00:32:00-07:00"),
odt3: new Date("1979-05-27T00:32:00.999999-07:00"),
odt4: new Date("1979-05-27 07:32:00Z"),
ld1: new Date("1979-05-27"),
lt1: "07:32:00",
lt2: "00:32:00.999999"
}
};
const actual = parseFile(path.join(testFilesDir, "datetime.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Inline Table",
fn(): void {
const expected = {
inlinetable: {
nile: {
also: {
malevolant: {
creation: {
drum: {
kit: "Tama"
}
}
}
},
derek: {
roddy: "drummer"
}
},
name: {
first: "Tom",
last: "Preston-Werner"
},
point: {
x: 1,
y: 2
},
dog: {
type: {
name: "pug"
}
},
"tosin.abasi": "guitarist",
animal: {
as: {
leaders: "tosin"
}
}
}
};
const actual = parseFile(path.join(testFilesDir, "inlineTable.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Array of Tables",
fn(): void {
const expected = {
bin: [
{ name: "deno", path: "cli/main.rs" },
{ name: "deno_core", path: "src/foo.rs" }
],
nib: [{ name: "node", path: "not_found" }]
};
const actual = parseFile(path.join(testFilesDir, "arrayTable.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Cargo",
fn(): void {
/* eslint-disable @typescript-eslint/camelcase */
const expected = {
workspace: { members: ["./", "core"] },
bin: [{ name: "deno", path: "cli/main.rs" }],
package: { name: "deno", version: "0.3.4", edition: "2018" },
dependencies: {
deno_core: { path: "./core" },
ansi_term: "0.11.0",
atty: "0.2.11",
dirs: "1.0.5",
flatbuffers: "0.5.0",
futures: "0.1.25",
getopts: "0.2.18",
http: "0.1.16",
hyper: "0.12.24",
"hyper-rustls": "0.16.0",
"integer-atomics": "1.0.2",
lazy_static: "1.3.0",
libc: "0.2.49",
log: "0.4.6",
rand: "0.6.5",
regex: "1.1.0",
remove_dir_all: "0.5.1",
ring: "0.14.6",
rustyline: "3.0.0",
serde_json: "1.0.38",
"source-map-mappings": "0.5.0",
tempfile: "3.0.7",
tokio: "0.1.15",
"tokio-executor": "0.1.6",
"tokio-fs": "0.1.5",
"tokio-io": "0.1.11",
"tokio-process": "0.2.3",
"tokio-threadpool": "0.1.11",
url: "1.7.2"
},
target: { "cfg(windows)": { dependencies: { winapi: "0.3.6" } } }
};
/* eslint-enable @typescript-eslint/camelcase */
const actual = parseFile(path.join(testFilesDir, "cargo.toml"));
assertEquals(actual, expected);
}
});
test({
name: "[TOML] Stringify",
fn(): void {
const src = {
foo: { bar: "deno" },
this: { is: { nested: "denonono" } },
"https://deno.land/std": {
$: "doller"
},
"##": {
deno: {
"https://deno.land": {
proto: "https",
":80": "port"
}
}
},
arrayObjects: [{ stuff: "in" }, {}, { the: "array" }],
deno: "is",
not: "[node]",
regex: "<ic*s*>",
NANI: "何?!",
comment: "Comment inside # the comment",
int1: 99,
int2: 42,
int3: 0,
int4: -17,
int5: 1000,
int6: 5349221,
int7: 12345,
flt1: 1.0,
flt2: 3.1415,
flt3: -0.01,
flt4: 5e22,
flt5: 1e6,
flt6: -2e-2,
flt7: 6.626e-34,
odt1: new Date("1979-05-01T07:32:00Z"),
odt2: new Date("1979-05-27T00:32:00-07:00"),
odt3: new Date("1979-05-27T00:32:00.999999-07:00"),
odt4: new Date("1979-05-27 07:32:00Z"),
ld1: new Date("1979-05-27"),
reg: /foo[bar]/,
sf1: Infinity,
sf2: Infinity,
sf3: -Infinity,
sf4: NaN,
sf5: NaN,
sf6: NaN,
data: [["gamma", "delta"], [1, 2]],
hosts: ["alpha", "omega"]
};
const expected = `deno = "is"
not = "[node]"
regex = "<ic*s*>"
NANI = "何?!"
comment = "Comment inside # the comment"
int1 = 99
int2 = 42
int3 = 0
int4 = -17
int5 = 1000
int6 = 5349221
int7 = 12345
flt1 = 1
flt2 = 3.1415
flt3 = -0.01
flt4 = 5e+22
flt5 = 1000000
flt6 = -0.02
flt7 = 6.626e-34
odt1 = 1979-05-01T07:32:00.000
odt2 = 1979-05-27T07:32:00.000
odt3 = 1979-05-27T07:32:00.999
odt4 = 1979-05-27T07:32:00.000
ld1 = 1979-05-27T00:00:00.000
reg = "/foo[bar]/"
sf1 = inf
sf2 = inf
sf3 = -inf
sf4 = NaN
sf5 = NaN
sf6 = NaN
data = [["gamma","delta"],[1,2]]
hosts = ["alpha","omega"]
[foo]
bar = "deno"
[this.is]
nested = "denonono"
["https://deno.land/std"]
"$" = "doller"
["##".deno."https://deno.land"]
proto = "https"
":80" = "port"
[[arrayObjects]]
stuff = "in"
[[arrayObjects]]
[[arrayObjects]]
the = "array"
`;
const actual = stringify(src);
assertEquals(actual, expected);
}
});
runIfMain(import.meta);

47
std/examples/README.md Normal file
View file

@ -0,0 +1,47 @@
# Deno example programs
This module contains small scripts that demonstrate use of Deno and its standard
module.
You can run these examples using just their URL or install the example as an
executable script which references the URL. (Think of installing as creating a
bookmark to a program.)
### A TCP echo server
```shell
deno https://deno.land/std/examples/echo_server.ts --allow-net
```
Or
```shell
deno install echo_server https://deno.land/std/examples/echo_server.ts --allow-net
```
### cat - print file to standard output
```shell
deno install deno_cat https://deno.land/std/examples/cat.ts --allow-read
deno_cat file.txt
```
### catj - print flattened JSON to standard output
A very useful command by Soheil Rashidi ported to Deno.
```shell
deno install catj https://deno.land/std/examples/catj.ts --allow-read
catj example.json
catj file1.json file2.json
echo example.json | catj -
```
### gist - easily create and upload Gists
```
export GIST_TOKEN=ABC # Generate at https://github.com/settings/tokens
deno install gist https://deno.land/std/examples/gist.ts --allow-net --allow-env
gist --title "Example gist 1" script.ts
gist --t "Example gist 2" script2.ts
```

10
std/examples/cat.ts Normal file
View file

@ -0,0 +1,10 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
async function cat(filenames: string[]): Promise<void> {
for (const filename of filenames) {
const file = await Deno.open(filename);
await Deno.copy(Deno.stdout, file);
file.close();
}
}
cat(Deno.args.slice(1));

110
std/examples/catj.ts Normal file

File diff suppressed because one or more lines are too long

4
std/examples/colors.ts Normal file
View file

@ -0,0 +1,4 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { bgBlue, red, bold, italic } from "../fmt/colors.ts";
console.log(bgBlue(italic(red(bold("Hello world!")))));

View file

@ -0,0 +1,9 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
const hostname = "0.0.0.0";
const port = 8080;
const listener = Deno.listen({ hostname, port });
console.log(`Listening on ${hostname}:${port}`);
while (true) {
const conn = await listener.accept();
Deno.copy(conn, conn);
}

65
std/examples/gist.ts Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env -S deno --allow-net --allow-env
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
const { args, env, exit, readFile } = Deno;
import { parse } from "https://deno.land/std/flags/mod.ts";
function pathBase(p: string): string {
const parts = p.split("/");
return parts[parts.length - 1];
}
async function main(): Promise<void> {
const token = env()["GIST_TOKEN"];
if (!token) {
console.error("GIST_TOKEN environmental variable not set.");
console.error("Get a token here: https://github.com/settings/tokens");
exit(1);
}
const parsedArgs = parse(args.slice(1));
if (parsedArgs._.length === 0) {
console.error(
"Usage: gist.ts --allow-env --allow-net [-t|--title Example] some_file " +
"[next_file]"
);
exit(1);
}
const files = {};
for (const filename of parsedArgs._) {
const base = pathBase(filename);
const content = await readFile(filename);
const contentStr = new TextDecoder().decode(content);
files[base] = { content: contentStr };
}
const content = {
description: parsedArgs.title || parsedArgs.t || "Example",
public: false,
files: files
};
const body = JSON.stringify(content);
const res = await fetch("https://api.github.com/gists", {
method: "POST",
headers: [
["Content-Type", "application/json"],
["User-Agent", "Deno-Gist"],
["Authorization", `token ${token}`]
],
body
});
if (res.ok) {
const resObj = await res.json();
console.log("Success");
console.log(resObj["html_url"]);
} else {
const err = await res.text();
console.error("Failure to POST", err);
}
}
main();

29
std/examples/test.ts Normal file
View file

@ -0,0 +1,29 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
const { run } = Deno;
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
/** Example of how to do basic tests */
test(function t1(): void {
assertEquals("hello", "hello");
});
test(function t2(): void {
assertEquals("world", "world");
});
/** A more complicated test that runs a subprocess. */
test(async function catSmoke(): Promise<void> {
const p = run({
args: [
Deno.execPath(),
"run",
"--allow-read",
"examples/cat.ts",
"README.md"
],
stdout: "piped"
});
const s = await p.status();
assertEquals(s.code, 0);
});

72
std/flags/README.md Normal file
View file

@ -0,0 +1,72 @@
# flags
Command line arguments parser for Deno based on minimist
# Example
```ts
const { args } = Deno;
import { parse } from "https://deno.land/std/flags/mod.ts";
console.dir(parse(args));
```
```
$ deno example.ts -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }
```
```
$ deno example.ts -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
x: 3,
y: 4,
n: 5,
a: true,
b: true,
c: true,
beep: 'boop' }
```
# API
## const parsedArgs = parse(args, options = {});
`parsedArgs._` contains all the arguments that didn't have an option associated
with them.
Numeric-looking arguments will be returned as numbers unless `options.string` or
`options.boolean` is set for that argument name.
Any arguments after `'--'` will not be parsed and will end up in `parsedArgs._`.
options can be:
- `options.string` - a string or array of strings argument names to always treat
as strings
- `options.boolean` - a boolean, string or array of strings to always treat as
booleans. if `true` will treat all double hyphenated arguments without equal
signs as boolean (e.g. affects `--foo`, not `-f` or `--foo=bar`)
- `options.alias` - an object mapping string names to strings or arrays of
string argument names to use as aliases
- `options.default` - an object mapping string argument names to default values
- `options.stopEarly` - when true, populate `parsedArgs._` with everything after
the first non-option
- `options['--']` - when true, populate `parsedArgs._` with everything before
the `--` and `parsedArgs['--']` with everything after the `--`. Here's an
example:
```ts
const { args } = Deno;
import { parse } from "https://deno.land/std/flags/mod.ts";
// options['--'] is now set to false
console.dir(parse(args, { "--": false }));
// $ deno example.ts -- a arg1
// output: { _: [ "example.ts", "a", "arg1" ] }
// options['--'] is now set to true
console.dir(parse(args, { "--": true }));
// $ deno example.ts -- a arg1
// output: { _: [ "example.ts" ], --: [ "a", "arg1" ] }
```
- `options.unknown` - a function which is invoked with a command line parameter
not defined in the `options` configuration object. If the function returns
`false`, the unknown option is not added to `parsedArgs`.

34
std/flags/all_bool_test.ts Executable file
View file

@ -0,0 +1,34 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
// flag boolean true (default all --args to boolean)
test(function flagBooleanTrue(): void {
const argv = parse(["moo", "--honk", "cow"], {
boolean: true
});
assertEquals(argv, {
honk: true,
_: ["moo", "cow"]
});
assertEquals(typeof argv.honk, "boolean");
});
// flag boolean true only affects double hyphen arguments without equals signs
test(function flagBooleanTrueOnlyAffectsDoubleDash(): void {
const argv = parse(["moo", "--honk", "cow", "-p", "55", "--tacos=good"], {
boolean: true
});
assertEquals(argv, {
honk: true,
tacos: "good",
p: 55,
_: ["moo", "cow"]
});
assertEquals(typeof argv.honk, "boolean");
});

168
std/flags/bool_test.ts Executable file
View file

@ -0,0 +1,168 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function flagBooleanDefaultFalse(): void {
const argv = parse(["moo"], {
boolean: ["t", "verbose"],
default: { verbose: false, t: false }
});
assertEquals(argv, {
verbose: false,
t: false,
_: ["moo"]
});
assertEquals(typeof argv.verbose, "boolean");
assertEquals(typeof argv.t, "boolean");
});
test(function booleanGroups(): void {
const argv = parse(["-x", "-z", "one", "two", "three"], {
boolean: ["x", "y", "z"]
});
assertEquals(argv, {
x: true,
y: false,
z: true,
_: ["one", "two", "three"]
});
assertEquals(typeof argv.x, "boolean");
assertEquals(typeof argv.y, "boolean");
assertEquals(typeof argv.z, "boolean");
});
test(function booleanAndAliasWithChainableApi(): void {
const aliased = ["-h", "derp"];
const regular = ["--herp", "derp"];
const aliasedArgv = parse(aliased, {
boolean: "herp",
alias: { h: "herp" }
});
const propertyArgv = parse(regular, {
boolean: "herp",
alias: { h: "herp" }
});
const expected = {
herp: true,
h: true,
_: ["derp"]
};
assertEquals(aliasedArgv, expected);
assertEquals(propertyArgv, expected);
});
test(function booleanAndAliasWithOptionsHash(): void {
const aliased = ["-h", "derp"];
const regular = ["--herp", "derp"];
const opts = {
alias: { h: "herp" },
boolean: "herp"
};
const aliasedArgv = parse(aliased, opts);
const propertyArgv = parse(regular, opts);
const expected = {
herp: true,
h: true,
_: ["derp"]
};
assertEquals(aliasedArgv, expected);
assertEquals(propertyArgv, expected);
});
test(function booleanAndAliasArrayWithOptionsHash(): void {
const aliased = ["-h", "derp"];
const regular = ["--herp", "derp"];
const alt = ["--harp", "derp"];
const opts = {
alias: { h: ["herp", "harp"] },
boolean: "h"
};
const aliasedArgv = parse(aliased, opts);
const propertyArgv = parse(regular, opts);
const altPropertyArgv = parse(alt, opts);
const expected = {
harp: true,
herp: true,
h: true,
_: ["derp"]
};
assertEquals(aliasedArgv, expected);
assertEquals(propertyArgv, expected);
assertEquals(altPropertyArgv, expected);
});
test(function booleanAndAliasUsingExplicitTrue(): void {
const aliased = ["-h", "true"];
const regular = ["--herp", "true"];
const opts = {
alias: { h: "herp" },
boolean: "h"
};
const aliasedArgv = parse(aliased, opts);
const propertyArgv = parse(regular, opts);
const expected = {
herp: true,
h: true,
_: []
};
assertEquals(aliasedArgv, expected);
assertEquals(propertyArgv, expected);
});
// regression, see https://github.com/substack/node-optimist/issues/71
// boolean and --x=true
test(function booleanAndNonBoolean(): void {
const parsed = parse(["--boool", "--other=true"], {
boolean: "boool"
});
assertEquals(parsed.boool, true);
assertEquals(parsed.other, "true");
const parsed2 = parse(["--boool", "--other=false"], {
boolean: "boool"
});
assertEquals(parsed2.boool, true);
assertEquals(parsed2.other, "false");
});
test(function booleanParsingTrue(): void {
const parsed = parse(["--boool=true"], {
default: {
boool: false
},
boolean: ["boool"]
});
assertEquals(parsed.boool, true);
});
test(function booleanParsingFalse(): void {
const parsed = parse(["--boool=false"], {
default: {
boool: true
},
boolean: ["boool"]
});
assertEquals(parsed.boool, false);
});
test(function booleanParsingTrueLike(): void {
const parsed = parse(["-t", "true123"], { boolean: ["t"] });
assertEquals(parsed.t, true);
const parsed2 = parse(["-t", "123"], { boolean: ["t"] });
assertEquals(parsed2.t, true);
const parsed3 = parse(["-t", "false123"], { boolean: ["t"] });
assertEquals(parsed3.t, true);
});

29
std/flags/dash_test.ts Executable file
View file

@ -0,0 +1,29 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function hyphen(): void {
assertEquals(parse(["-n", "-"]), { n: "-", _: [] });
assertEquals(parse(["-"]), { _: ["-"] });
assertEquals(parse(["-f-"]), { f: "-", _: [] });
assertEquals(parse(["-b", "-"], { boolean: "b" }), { b: true, _: ["-"] });
assertEquals(parse(["-s", "-"], { string: "s" }), { s: "-", _: [] });
});
test(function doubleDash(): void {
assertEquals(parse(["-a", "--", "b"]), { a: true, _: ["b"] });
assertEquals(parse(["--a", "--", "b"]), { a: true, _: ["b"] });
assertEquals(parse(["--a", "--", "b"]), { a: true, _: ["b"] });
});
test(function moveArgsAfterDoubleDashIntoOwnArray(): void {
assertEquals(
parse(["--name", "John", "before", "--", "after"], { "--": true }),
{
name: "John",
_: ["before"],
"--": ["after"]
}
);
});

33
std/flags/default_bool_test.ts Executable file
View file

@ -0,0 +1,33 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function booleanDefaultTrue(): void {
const argv = parse([], {
boolean: "sometrue",
default: { sometrue: true }
});
assertEquals(argv.sometrue, true);
});
test(function booleanDefaultFalse(): void {
const argv = parse([], {
boolean: "somefalse",
default: { somefalse: false }
});
assertEquals(argv.somefalse, false);
});
test(function booleanDefaultNull(): void {
const argv = parse([], {
boolean: "maybe",
default: { maybe: null }
});
assertEquals(argv.maybe, null);
const argv2 = parse(["--maybe"], {
boolean: "maybe",
default: { maybe: null }
});
assertEquals(argv2.maybe, true);
});

24
std/flags/dotted_test.ts Executable file
View file

@ -0,0 +1,24 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function dottedAlias(): void {
const argv = parse(["--a.b", "22"], {
default: { "a.b": 11 },
alias: { "a.b": "aa.bb" }
});
assertEquals(argv.a.b, 22);
assertEquals(argv.aa.bb, 22);
});
test(function dottedDefault(): void {
const argv = parse([], { default: { "a.b": 11 }, alias: { "a.b": "aa.bb" } });
assertEquals(argv.a.b, 11);
assertEquals(argv.aa.bb, 11);
});
test(function dottedDefaultWithNoAlias(): void {
const argv = parse([], { default: { "a.b": 11 } });
assertEquals(argv.a.b, 11);
});

5
std/flags/example.ts Normal file
View file

@ -0,0 +1,5 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
const { args } = Deno;
import { parse } from "./mod.ts";
console.dir(parse(args));

14
std/flags/kv_short_test.ts Executable file
View file

@ -0,0 +1,14 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function short(): void {
const argv = parse(["-b=123"]);
assertEquals(argv, { b: 123, _: [] });
});
test(function multiShort(): void {
const argv = parse(["-a=whatever", "-b=robots"]);
assertEquals(argv, { a: "whatever", b: "robots", _: [] });
});

20
std/flags/long_test.ts Executable file
View file

@ -0,0 +1,20 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function longOpts(): void {
assertEquals(parse(["--bool"]), { bool: true, _: [] });
assertEquals(parse(["--pow", "xixxle"]), { pow: "xixxle", _: [] });
assertEquals(parse(["--pow=xixxle"]), { pow: "xixxle", _: [] });
assertEquals(parse(["--host", "localhost", "--port", "555"]), {
host: "localhost",
port: 555,
_: []
});
assertEquals(parse(["--host=localhost", "--port=555"]), {
host: "localhost",
port: 555,
_: []
});
});

317
std/flags/mod.ts Normal file
View file

@ -0,0 +1,317 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
export interface ArgParsingOptions {
unknown?: (i: unknown) => unknown;
boolean?: boolean | string | string[];
alias?: { [key: string]: string | string[] };
string?: string | string[];
default?: { [key: string]: unknown };
"--"?: boolean;
stopEarly?: boolean;
}
const DEFAULT_OPTIONS = {
unknown: (i: unknown): unknown => i,
boolean: false,
alias: {},
string: [],
default: {},
"--": false,
stopEarly: false
};
interface Flags {
bools: { [key: string]: boolean };
strings: { [key: string]: boolean };
unknownFn: (i: unknown) => unknown;
allBools: boolean;
}
interface NestedMapping {
[key: string]: NestedMapping | unknown;
}
function get<T>(obj: { [s: string]: T }, key: string): T | undefined {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return obj[key];
}
}
function isNumber(x: unknown): boolean {
if (typeof x === "number") return true;
if (/^0x[0-9a-f]+$/i.test(String(x))) return true;
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(String(x));
}
function hasKey(obj: NestedMapping, keys: string[]): boolean {
let o = obj;
keys.slice(0, -1).forEach(function(key: string): void {
o = (get(o, key) || {}) as NestedMapping;
});
const key = keys[keys.length - 1];
return key in o;
}
export function parse(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any[],
initialOptions?: ArgParsingOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): { [key: string]: any } {
const options: ArgParsingOptions = {
...DEFAULT_OPTIONS,
...(initialOptions || {})
};
const flags: Flags = {
bools: {},
strings: {},
unknownFn: options.unknown!,
allBools: false
};
if (options.boolean !== undefined) {
if (typeof options.boolean === "boolean") {
flags.allBools = !!options.boolean;
} else {
const booleanArgs: string[] =
typeof options.boolean === "string"
? [options.boolean]
: options.boolean;
booleanArgs.filter(Boolean).forEach((key: string): void => {
flags.bools[key] = true;
});
}
}
const aliases: { [key: string]: string[] } = {};
if (options.alias !== undefined) {
for (const key in options.alias) {
const val = get(options.alias, key)!;
if (typeof val === "string") {
aliases[key] = [val];
} else {
aliases[key] = val;
}
for (const alias of get(aliases, key)!) {
aliases[alias] = [key].concat(
aliases[key].filter((y: string): boolean => alias !== y)
);
}
}
}
if (options.string !== undefined) {
const stringArgs =
typeof options.string === "string" ? [options.string] : options.string;
stringArgs.filter(Boolean).forEach(function(key): void {
flags.strings[key] = true;
const alias = get(aliases, key);
if (alias) {
alias.forEach((alias: string): void => {
flags.strings[alias] = true;
});
}
});
}
const defaults = options.default!;
const argv: { [key: string]: unknown[] } = { _: [] };
function argDefined(key: string, arg: string): boolean {
return (
(flags.allBools && /^--[^=]+$/.test(arg)) ||
get(flags.bools, key) ||
!!get(flags.strings, key) ||
!!get(aliases, key)
);
}
function setKey(obj: NestedMapping, keys: string[], value: unknown): void {
let o = obj;
keys.slice(0, -1).forEach(function(key): void {
if (get(o, key) === undefined) {
o[key] = {};
}
o = get(o, key) as NestedMapping;
});
const key = keys[keys.length - 1];
if (
get(o, key) === undefined ||
get(flags.bools, key) ||
typeof get(o, key) === "boolean"
) {
o[key] = value;
} else if (Array.isArray(get(o, key))) {
(o[key] as unknown[]).push(value);
} else {
o[key] = [get(o, key), value];
}
}
function setArg(
key: string,
val: unknown,
arg: string | undefined = undefined
): void {
if (arg && flags.unknownFn && !argDefined(key, arg)) {
if (flags.unknownFn(arg) === false) return;
}
const value = !get(flags.strings, key) && isNumber(val) ? Number(val) : val;
setKey(argv, key.split("."), value);
(get(aliases, key) || []).forEach(function(x): void {
setKey(argv, x.split("."), value);
});
}
function aliasIsBoolean(key: string): boolean {
return get(aliases, key)!.some(function(x): boolean {
return get(flags.bools, x)!;
});
}
Object.keys(flags.bools).forEach(function(key): void {
setArg(key, defaults[key] === undefined ? false : defaults[key]);
});
let notFlags: string[] = [];
// all args after "--" are not parsed
if (args.indexOf("--") !== -1) {
notFlags = args.slice(args.indexOf("--") + 1);
args = args.slice(0, args.indexOf("--"));
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (/^--.+=/.test(arg)) {
// Using [\s\S] instead of . because js doesn't support the
// 'dotall' regex modifier. See:
// http://stackoverflow.com/a/1068308/13216
const m = arg.match(/^--([^=]+)=([\s\S]*)$/)!;
const key = m[1];
const value = m[2];
if (flags.bools[key]) {
const booleanValue = value !== "false";
setArg(key, booleanValue, arg);
} else {
setArg(key, value, arg);
}
} else if (/^--no-.+/.test(arg)) {
const key = arg.match(/^--no-(.+)/)![1];
setArg(key, false, arg);
} else if (/^--.+/.test(arg)) {
const key = arg.match(/^--(.+)/)![1];
const next = args[i + 1];
if (
next !== undefined &&
!/^-/.test(next) &&
!get(flags.bools, key) &&
!flags.allBools &&
(get(aliases, key) ? !aliasIsBoolean(key) : true)
) {
setArg(key, next, arg);
i++;
} else if (/^(true|false)$/.test(next)) {
setArg(key, next === "true", arg);
i++;
} else {
setArg(key, get(flags.strings, key) ? "" : true, arg);
}
} else if (/^-[^-]+/.test(arg)) {
const letters = arg.slice(1, -1).split("");
let broken = false;
for (let j = 0; j < letters.length; j++) {
const next = arg.slice(j + 2);
if (next === "-") {
setArg(letters[j], next, arg);
continue;
}
if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) {
setArg(letters[j], next.split("=")[1], arg);
broken = true;
break;
}
if (
/[A-Za-z]/.test(letters[j]) &&
/-?\d+(\.\d*)?(e-?\d+)?$/.test(next)
) {
setArg(letters[j], next, arg);
broken = true;
break;
}
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
setArg(letters[j], arg.slice(j + 2), arg);
broken = true;
break;
} else {
setArg(letters[j], get(flags.strings, letters[j]) ? "" : true, arg);
}
}
const key = arg.slice(-1)[0];
if (!broken && key !== "-") {
if (
args[i + 1] &&
!/^(-|--)[^-]/.test(args[i + 1]) &&
!get(flags.bools, key) &&
(get(aliases, key) ? !aliasIsBoolean(key) : true)
) {
setArg(key, args[i + 1], arg);
i++;
} else if (args[i + 1] && /^(true|false)$/.test(args[i + 1])) {
setArg(key, args[i + 1] === "true", arg);
i++;
} else {
setArg(key, get(flags.strings, key) ? "" : true, arg);
}
}
} else {
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings["_"] || !isNumber(arg) ? arg : Number(arg));
}
if (options.stopEarly) {
argv._.push(...args.slice(i + 1));
break;
}
}
}
Object.keys(defaults).forEach(function(key): void {
if (!hasKey(argv, key.split("."))) {
setKey(argv, key.split("."), defaults[key]);
(aliases[key] || []).forEach(function(x): void {
setKey(argv, x.split("."), defaults[key]);
});
}
});
if (options["--"]) {
argv["--"] = [];
notFlags.forEach(function(key): void {
argv["--"].push(key);
});
} else {
notFlags.forEach(function(key): void {
argv._.push(key);
});
}
return argv;
}

41
std/flags/num_test.ts Executable file
View file

@ -0,0 +1,41 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function nums(): void {
const argv = parse([
"-x",
"1234",
"-y",
"5.67",
"-z",
"1e7",
"-w",
"10f",
"--hex",
"0xdeadbeef",
"789"
]);
assertEquals(argv, {
x: 1234,
y: 5.67,
z: 1e7,
w: "10f",
hex: 0xdeadbeef,
_: [789]
});
assertEquals(typeof argv.x, "number");
assertEquals(typeof argv.y, "number");
assertEquals(typeof argv.z, "number");
assertEquals(typeof argv.w, "string");
assertEquals(typeof argv.hex, "number");
assertEquals(typeof argv._[0], "number");
});
test(function alreadyNumber(): void {
const argv = parse(["-x", 1234, 789]);
assertEquals(argv, { x: 1234, _: [789] });
assertEquals(typeof argv.x, "number");
assertEquals(typeof argv._[0], "number");
});

201
std/flags/parse_test.ts Executable file
View file

@ -0,0 +1,201 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function _arseArgs(): void {
assertEquals(parse(["--no-moo"]), { moo: false, _: [] });
assertEquals(parse(["-v", "a", "-v", "b", "-v", "c"]), {
v: ["a", "b", "c"],
_: []
});
});
test(function comprehensive(): void {
assertEquals(
parse([
"--name=meowmers",
"bare",
"-cats",
"woo",
"-h",
"awesome",
"--multi=quux",
"--key",
"value",
"-b",
"--bool",
"--no-meep",
"--multi=baz",
"--",
"--not-a-flag",
"eek"
]),
{
c: true,
a: true,
t: true,
s: "woo",
h: "awesome",
b: true,
bool: true,
key: "value",
multi: ["quux", "baz"],
meep: false,
name: "meowmers",
_: ["bare", "--not-a-flag", "eek"]
}
);
});
test(function flagBoolean(): void {
const argv = parse(["-t", "moo"], { boolean: "t" });
assertEquals(argv, { t: true, _: ["moo"] });
assertEquals(typeof argv.t, "boolean");
});
test(function flagBooleanValue(): void {
const argv = parse(["--verbose", "false", "moo", "-t", "true"], {
boolean: ["t", "verbose"],
default: { verbose: true }
});
assertEquals(argv, {
verbose: false,
t: true,
_: ["moo"]
});
assertEquals(typeof argv.verbose, "boolean");
assertEquals(typeof argv.t, "boolean");
});
test(function newlinesInParams(): void {
const args = parse(["-s", "X\nX"]);
assertEquals(args, { _: [], s: "X\nX" });
// reproduce in bash:
// VALUE="new
// line"
// deno program.js --s="$VALUE"
const args2 = parse(["--s=X\nX"]);
assertEquals(args2, { _: [], s: "X\nX" });
});
test(function strings(): void {
const s = parse(["-s", "0001234"], { string: "s" }).s;
assertEquals(s, "0001234");
assertEquals(typeof s, "string");
const x = parse(["-x", "56"], { string: "x" }).x;
assertEquals(x, "56");
assertEquals(typeof x, "string");
});
test(function stringArgs(): void {
const s = parse([" ", " "], { string: "_" })._;
assertEquals(s.length, 2);
assertEquals(typeof s[0], "string");
assertEquals(s[0], " ");
assertEquals(typeof s[1], "string");
assertEquals(s[1], " ");
});
test(function emptyStrings(): void {
const s = parse(["-s"], { string: "s" }).s;
assertEquals(s, "");
assertEquals(typeof s, "string");
const str = parse(["--str"], { string: "str" }).str;
assertEquals(str, "");
assertEquals(typeof str, "string");
const letters = parse(["-art"], {
string: ["a", "t"]
});
assertEquals(letters.a, "");
assertEquals(letters.r, true);
assertEquals(letters.t, "");
});
test(function stringAndAlias(): void {
const x = parse(["--str", "000123"], {
string: "s",
alias: { s: "str" }
});
assertEquals(x.str, "000123");
assertEquals(typeof x.str, "string");
assertEquals(x.s, "000123");
assertEquals(typeof x.s, "string");
const y = parse(["-s", "000123"], {
string: "str",
alias: { str: "s" }
});
assertEquals(y.str, "000123");
assertEquals(typeof y.str, "string");
assertEquals(y.s, "000123");
assertEquals(typeof y.s, "string");
});
test(function slashBreak(): void {
assertEquals(parse(["-I/foo/bar/baz"]), { I: "/foo/bar/baz", _: [] });
assertEquals(parse(["-xyz/foo/bar/baz"]), {
x: true,
y: true,
z: "/foo/bar/baz",
_: []
});
});
test(function alias(): void {
const argv = parse(["-f", "11", "--zoom", "55"], {
alias: { z: "zoom" }
});
assertEquals(argv.zoom, 55);
assertEquals(argv.z, argv.zoom);
assertEquals(argv.f, 11);
});
test(function multiAlias(): void {
const argv = parse(["-f", "11", "--zoom", "55"], {
alias: { z: ["zm", "zoom"] }
});
assertEquals(argv.zoom, 55);
assertEquals(argv.z, argv.zoom);
assertEquals(argv.z, argv.zm);
assertEquals(argv.f, 11);
});
test(function nestedDottedObjects(): void {
const argv = parse([
"--foo.bar",
"3",
"--foo.baz",
"4",
"--foo.quux.quibble",
"5",
"--foo.quux.oO",
"--beep.boop"
]);
assertEquals(argv.foo, {
bar: 3,
baz: 4,
quux: {
quibble: 5,
oO: true
}
});
assertEquals(argv.beep, { boop: true });
});
test(function flagBuiltinProperty(): void {
const argv = parse(["--toString", "--valueOf", "foo"]);
assertEquals(argv, { toString: true, valueOf: "foo", _: [] });
assertEquals(typeof argv.toString, "boolean");
assertEquals(typeof argv.valueOf, "string");
});

46
std/flags/short_test.ts Executable file
View file

@ -0,0 +1,46 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function numbericShortArgs(): void {
assertEquals(parse(["-n123"]), { n: 123, _: [] });
assertEquals(parse(["-123", "456"]), { 1: true, 2: true, 3: 456, _: [] });
});
test(function short(): void {
assertEquals(parse(["-b"]), { b: true, _: [] });
assertEquals(parse(["foo", "bar", "baz"]), { _: ["foo", "bar", "baz"] });
assertEquals(parse(["-cats"]), { c: true, a: true, t: true, s: true, _: [] });
assertEquals(parse(["-cats", "meow"]), {
c: true,
a: true,
t: true,
s: "meow",
_: []
});
assertEquals(parse(["-h", "localhost"]), { h: "localhost", _: [] });
assertEquals(parse(["-h", "localhost", "-p", "555"]), {
h: "localhost",
p: 555,
_: []
});
});
test(function mixedShortBoolAndCapture(): void {
assertEquals(parse(["-h", "localhost", "-fp", "555", "script.js"]), {
f: true,
p: 555,
h: "localhost",
_: ["script.js"]
});
});
test(function shortAndLong(): void {
assertEquals(parse(["-h", "localhost", "-fp", "555", "script.js"]), {
f: true,
p: 555,
h: "localhost",
_: ["script.js"]
});
});

16
std/flags/stop_early_test.ts Executable file
View file

@ -0,0 +1,16 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
// stops parsing on the first non-option when stopEarly is set
test(function stopParsing(): void {
const argv = parse(["--aaa", "bbb", "ccc", "--ddd"], {
stopEarly: true
});
assertEquals(argv, {
aaa: "bbb",
_: ["ccc", "--ddd"]
});
});

98
std/flags/unknown_test.ts Executable file
View file

@ -0,0 +1,98 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function booleanAndAliasIsNotUnknown(): void {
const unknown: unknown[] = [];
function unknownFn(arg: unknown): boolean {
unknown.push(arg);
return false;
}
const aliased = ["-h", "true", "--derp", "true"];
const regular = ["--herp", "true", "-d", "true"];
const opts = {
alias: { h: "herp" },
boolean: "h",
unknown: unknownFn
};
parse(aliased, opts);
parse(regular, opts);
assertEquals(unknown, ["--derp", "-d"]);
});
test(function flagBooleanTrueAnyDoubleHyphenArgumentIsNotUnknown(): void {
const unknown: unknown[] = [];
function unknownFn(arg: unknown): boolean {
unknown.push(arg);
return false;
}
const argv = parse(["--honk", "--tacos=good", "cow", "-p", "55"], {
boolean: true,
unknown: unknownFn
});
assertEquals(unknown, ["--tacos=good", "cow", "-p"]);
assertEquals(argv, {
honk: true,
_: []
});
});
test(function stringAndAliasIsNotUnkown(): void {
const unknown: unknown[] = [];
function unknownFn(arg: unknown): boolean {
unknown.push(arg);
return false;
}
const aliased = ["-h", "hello", "--derp", "goodbye"];
const regular = ["--herp", "hello", "-d", "moon"];
const opts = {
alias: { h: "herp" },
string: "h",
unknown: unknownFn
};
parse(aliased, opts);
parse(regular, opts);
assertEquals(unknown, ["--derp", "-d"]);
});
test(function defaultAndAliasIsNotUnknown(): void {
const unknown: unknown[] = [];
function unknownFn(arg: unknown): boolean {
unknown.push(arg);
return false;
}
const aliased = ["-h", "hello"];
const regular = ["--herp", "hello"];
const opts = {
default: { h: "bar" },
alias: { h: "herp" },
unknown: unknownFn
};
parse(aliased, opts);
parse(regular, opts);
assertEquals(unknown, []);
});
test(function valueFollowingDoubleHyphenIsNotUnknown(): void {
const unknown: unknown[] = [];
function unknownFn(arg: unknown): boolean {
unknown.push(arg);
return false;
}
const aliased = ["--bad", "--", "good", "arg"];
const opts = {
"--": true,
unknown: unknownFn
};
const argv = parse(aliased, opts);
assertEquals(unknown, ["--bad"]);
assertEquals(argv, {
"--": ["good", "arg"],
_: []
});
});

8
std/flags/whitespace_test.ts Executable file
View file

@ -0,0 +1,8 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { parse } from "./mod.ts";
test(function whitespaceShouldBeWhitespace(): void {
assertEquals(parse(["-x", "\t"]).x, "\t");
});

212
std/fmt/README.md Normal file
View file

@ -0,0 +1,212 @@
# Printf for Deno
This is very much a work-in-progress. I'm actively soliciting feedback.
What immediately follows are points for discussion.
If you are looking for the documentation proper, skip to:
"printf: prints formatted output"
below.
## Discussion
This is very much a work-in-progress. I'm actively soliciting feedback.
- What useful features are available in other languages apart from
Golang and C?
- behaviour of `%v` verb. In Golang, this is a shortcut verb to "print the
default format" of the argument. It is currently implemented to format
using `toString` in the default case and `inpect` if the `%#v`
alternative format flag is used in the format directive. Alternativly,
`%V` could be used to distinguish the two.
`inspect` output is not defined, however. This may be problematic if using
this code on other plattforms (and expecting interoperability). To my
knowledge, no suitable specification of object representation aside from JSON
and `toString` exist. ( Aside: see "[Common object formats][3]" in the
"Console Living Standard" which basically says "do whatever" )
- `%j` verb. This is an extension particular to this implementation. Currently
not very sophisticated, it just runs `JSON.stringify` on the argument.
Consider possible modifier flags, etc.
- `<` verb. This is an extension that assumes the argument is an array and will
format each element according to the format (surrounded by [] and seperated
by comma) (`<` Mnemonic: pull each element out of array)
- how to deal with more newfangled Javascript features ( generic Iterables,
Map and Set types, typed Arrays, ...)
- the implementation is fairly rough around the edges:
- currently contains little in the way of checking for
correctness. Conceivably, there will be a 'strict' form, e.g.
that ensures only Number-ish arguments are passed to %f flags
- assembles output using string concatenation instead of
utilizing buffers or other optimizations. It would be nice to
have printf / sprintf / fprintf (etc) all in one.
- float formatting is handled by toString() and to `toExponential`
along with a mess of Regexp. Would be nice to use fancy match
- some flags that are potentially applicable ( POSIX long and unsigned
modifiers are not likely useful) are missing, namely %q (print quoted), %U
(unicode format)
## Author
Tim Becker (tim@presseverykey.com)
## License
MIT
The implementation is inspired by POSIX and Golang (see above) but does
not port implementation code. A number of Golang test-cases based on:
https://golang.org/src/fmt/fmt_test.go
( BSD: Copyright (c) 2009 The Go Authors. All rights reserved. )
were used.
# printf: prints formatted output
sprintf converts and formats a variable number of arguments as is
specified by a `format string`. In it's basic form, a format string
may just be a literal. In case arguments are meant to be formatted,
a `directive` is contained in the format string, preceded by a '%' character:
%<verb>
E.g. the verb `s` indicates the directive should be replaced by the
string representation of the argument in the corresponding position of
the argument list. E.g.:
Hello %s!
applied to the arguments "World" yields "Hello World!"
The meaning of the format string is modelled after [POSIX][1] format
strings as well as well as [Golang format strings][2]. Both contain
elements specific to the respective programming language that don't
apply to JavaScript, so they can not be fully supported. Furthermore we
implement some functionality that is specific to JS.
## Verbs
The following verbs are supported:
| Verb | Meaning |
| ----- | -------------------------------------------------------------- |
| `%` | print a literal percent |
| `t` | evaluate arg as boolean, print `true` or `false` |
| `b` | eval as number, print binary |
| `c` | eval as number, print character corresponding to the codePoint |
| `o` | eval as number, print octal |
| `x X` | print as hex (ff FF), treat string as list of bytes |
| `e E` | print number in scientific/exponent format 1.123123e+01 |
| `f F` | print number as float with decimal point and no exponent |
| `g G` | use %e %E or %f %F depending on size of argument |
| `s` | interpolate string |
| `T` | type of arg, as returned by `typeof` |
| `v` | value of argument in 'default' format (see below) |
| `j` | argument as formatted by `JSON.stringify` |
## Width and Precision
Verbs may be modified by providing them with width and precision, either or
both may be omitted:
%9f width 9, default precision
%.9f default width, precision 9
%8.9f width 8, precision 9
%8.f width 9, precision 0
In general, 'width' describes the minimum length of the output, while 'precision'
limits the output.
| verb | precision |
| --------- | -------------------------------------------------------------- |
| `t` | n/a |
| `b c o` | n/a |
| `x X` | n/a for number, strings are truncated to p bytes(!) |
| `e E f F` | number of places after decimal, default 6 |
| `g G` | set maximum number of digits |
| `s` | truncate input |
| `T` | truncate |
| `v` | tuncate, or depth if used with # see "'default' format", below |
| `j` | n/a |
Numerical values for width and precision can be substituted for the `*` char, in
which case the values are obtained from the next args, e.g.:
sprintf ("%*.*f", 9,8,456.0)
is equivalent to
sprintf ("%9.9f", 456.0)
## Flags
The effects of the verb may be further influenced by using flags to modify the
directive:
| Flag | Verb | Meaning |
| ----- | --------- | -------------------------------------------------------------------------- |
| `+` | numeric | always print sign |
| `-` | all | pad to the right (left justify) |
| `#` | | alternate format |
| `#` | `b o x X` | prefix with `0b 0 0x` |
| `#` | `g G` | don't remove trailing zeros |
| `#` | `v` | ues output of `inspect` instead of `toString` |
| `' '` | | space character |
| `' '` | `x X` | leave spaces between bytes when printing string |
| `' '` | `d` | insert space for missing `+` sign character |
| `0` | all | pad with zero, `-` takes precedence, sign is appended in front of padding |
| `<` | all | format elements of the passed array according to the directive (extension) |
## 'default' format
The default format used by `%v` is the result of calling `toString()` on the
relevant argument. If the `#` flags is used, the result of calling `inspect()`
is interpolated. In this case, the precision, if set is passed to `inspect()` as
the 'depth' config parameter
## Positional arguments
Arguments do not need to be consumed in the order they are provded and may
be consumed more than once. E.g.:
sprintf("%[2]s %[1]s", "World", "Hello")
returns "Hello World". The precence of a positional indicator resets the arg counter
allowing args to be reused:
sprintf("dec[%d]=%d hex[%[1]d]=%x oct[%[1]d]=%#o %s", 1, 255, "Third")
returns `dec[1]=255 hex[1]=0xff oct[1]=0377 Third`
Width and precision my also use positionals:
"%[2]*.[1]*d", 1, 2
This follows the golang conventions and not POSIX.
## Errors
The following errors are handled:
Incorrect verb:
S("%h", "") %!(BAD VERB 'h')
Too few arguments:
S("%d") %!(MISSING 'd')"
[1]: https://pubs.opengroup.org/onlinepubs/009695399/functions/fprintf.html
[2]: https://golang.org/pkg/fmt/
[3]: https://console.spec.whatwg.org/#object-formats

12
std/fmt/TODO Normal file
View file

@ -0,0 +1,12 @@
* "native" formatting, json, arrays, object/structs, functions ...
* %q %U
* Java has a %n flag to print the plattform native newline... in POSIX
that means "number of chars printed so far", though.
* use of Writer and Buffer internally in order to make FPrintf, Printf, etc.
easier and more elegant.
* see "Discussion" in README
*scanf , pack,unpack, annotated hex
* error handling, consistantly
* probably rewrite, now that I konw how it's done.

148
std/fmt/colors.ts Normal file
View file

@ -0,0 +1,148 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
/**
* A module to print ANSI terminal colors. Inspired by chalk, kleur, and colors
* on npm.
*
* ```
* import { bgBlue, red, bold } from "https://deno.land/std/fmt/colors.ts";
* console.log(bgBlue(red(bold("Hello world!"))));
* ```
*
* This module supports `NO_COLOR` environmental variable disabling any coloring
* if `NO_COLOR` is set.
*/
const { noColor } = Deno;
interface Code {
open: string;
close: string;
regexp: RegExp;
}
let enabled = !noColor;
export function setEnabled(value: boolean): void {
if (noColor) {
return;
}
enabled = value;
}
export function getEnabled(): boolean {
return enabled;
}
function code(open: number, close: number): Code {
return {
open: `\x1b[${open}m`,
close: `\x1b[${close}m`,
regexp: new RegExp(`\\x1b\\[${close}m`, "g")
};
}
function run(str: string, code: Code): string {
return enabled
? `${code.open}${str.replace(code.regexp, code.open)}${code.close}`
: str;
}
export function reset(str: string): string {
return run(str, code(0, 0));
}
export function bold(str: string): string {
return run(str, code(1, 22));
}
export function dim(str: string): string {
return run(str, code(2, 22));
}
export function italic(str: string): string {
return run(str, code(3, 23));
}
export function underline(str: string): string {
return run(str, code(4, 24));
}
export function inverse(str: string): string {
return run(str, code(7, 27));
}
export function hidden(str: string): string {
return run(str, code(8, 28));
}
export function strikethrough(str: string): string {
return run(str, code(9, 29));
}
export function black(str: string): string {
return run(str, code(30, 39));
}
export function red(str: string): string {
return run(str, code(31, 39));
}
export function green(str: string): string {
return run(str, code(32, 39));
}
export function yellow(str: string): string {
return run(str, code(33, 39));
}
export function blue(str: string): string {
return run(str, code(34, 39));
}
export function magenta(str: string): string {
return run(str, code(35, 39));
}
export function cyan(str: string): string {
return run(str, code(36, 39));
}
export function white(str: string): string {
return run(str, code(37, 39));
}
export function gray(str: string): string {
return run(str, code(90, 39));
}
export function bgBlack(str: string): string {
return run(str, code(40, 49));
}
export function bgRed(str: string): string {
return run(str, code(41, 49));
}
export function bgGreen(str: string): string {
return run(str, code(42, 49));
}
export function bgYellow(str: string): string {
return run(str, code(43, 49));
}
export function bgBlue(str: string): string {
return run(str, code(44, 49));
}
export function bgMagenta(str: string): string {
return run(str, code(45, 49));
}
export function bgCyan(str: string): string {
return run(str, code(46, 49));
}
export function bgWhite(str: string): string {
return run(str, code(47, 49));
}

121
std/fmt/colors_test.ts Normal file
View file

@ -0,0 +1,121 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import * as c from "./colors.ts";
import "../examples/colors.ts";
test(function singleColor(): void {
assertEquals(c.red("foo bar"), "foo bar");
});
test(function doubleColor(): void {
assertEquals(c.bgBlue(c.red("foo bar")), "foo bar");
});
test(function replacesCloseCharacters(): void {
assertEquals(c.red("Hello"), "Hello");
});
test(function enablingColors(): void {
assertEquals(c.getEnabled(), true);
c.setEnabled(false);
assertEquals(c.bgBlue(c.red("foo bar")), "foo bar");
c.setEnabled(true);
assertEquals(c.red("foo bar"), "foo bar");
});
test(function testBold(): void {
assertEquals(c.bold("foo bar"), "foo bar");
});
test(function testDim(): void {
assertEquals(c.dim("foo bar"), "foo bar");
});
test(function testItalic(): void {
assertEquals(c.italic("foo bar"), "foo bar");
});
test(function testUnderline(): void {
assertEquals(c.underline("foo bar"), "foo bar");
});
test(function testInverse(): void {
assertEquals(c.inverse("foo bar"), "foo bar");
});
test(function testHidden(): void {
assertEquals(c.hidden("foo bar"), "foo bar");
});
test(function testStrikethrough(): void {
assertEquals(c.strikethrough("foo bar"), "foo bar");
});
test(function testBlack(): void {
assertEquals(c.black("foo bar"), "foo bar");
});
test(function testRed(): void {
assertEquals(c.red("foo bar"), "foo bar");
});
test(function testGreen(): void {
assertEquals(c.green("foo bar"), "foo bar");
});
test(function testYellow(): void {
assertEquals(c.yellow("foo bar"), "foo bar");
});
test(function testBlue(): void {
assertEquals(c.blue("foo bar"), "foo bar");
});
test(function testMagenta(): void {
assertEquals(c.magenta("foo bar"), "foo bar");
});
test(function testCyan(): void {
assertEquals(c.cyan("foo bar"), "foo bar");
});
test(function testWhite(): void {
assertEquals(c.white("foo bar"), "foo bar");
});
test(function testGray(): void {
assertEquals(c.gray("foo bar"), "foo bar");
});
test(function testBgBlack(): void {
assertEquals(c.bgBlack("foo bar"), "foo bar");
});
test(function testBgRed(): void {
assertEquals(c.bgRed("foo bar"), "foo bar");
});
test(function testBgGreen(): void {
assertEquals(c.bgGreen("foo bar"), "foo bar");
});
test(function testBgYellow(): void {
assertEquals(c.bgYellow("foo bar"), "foo bar");
});
test(function testBgBlue(): void {
assertEquals(c.bgBlue("foo bar"), "foo bar");
});
test(function testBgMagenta(): void {
assertEquals(c.bgMagenta("foo bar"), "foo bar");
});
test(function testBgCyan(): void {
assertEquals(c.bgCyan("foo bar"), "foo bar");
});
test(function testBgWhite(): void {
assertEquals(c.bgWhite("foo bar"), "foo bar");
});

674
std/fmt/sprintf.ts Normal file
View file

@ -0,0 +1,674 @@
enum State {
PASSTHROUGH,
PERCENT,
POSITIONAL,
PRECISION,
WIDTH
}
enum WorP {
WIDTH,
PRECISION
}
class Flags {
plus?: boolean;
dash?: boolean;
sharp?: boolean;
space?: boolean;
zero?: boolean;
lessthan?: boolean;
width = -1;
precision = -1;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const min = Math.min;
const UNICODE_REPLACEMENT_CHARACTER = "\ufffd";
const DEFAULT_PRECISION = 6;
const FLOAT_REGEXP = /(-?)(\d)\.?(\d*)e([+-])(\d+)/;
enum F {
sign = 1,
mantissa,
fractional,
esign,
exponent
}
class Printf {
format: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any[];
i: number;
state: State = State.PASSTHROUGH;
verb = "";
buf = "";
argNum = 0;
flags: Flags = new Flags();
haveSeen: boolean[];
// barf, store precision and width errors for later processing ...
tmpError?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(format: string, ...args: any[]) {
this.format = format;
this.args = args;
this.haveSeen = new Array(args.length);
this.i = 0;
}
doPrintf(): string {
for (; this.i < this.format.length; ++this.i) {
const c = this.format[this.i];
switch (this.state) {
case State.PASSTHROUGH:
if (c === "%") {
this.state = State.PERCENT;
} else {
this.buf += c;
}
break;
case State.PERCENT:
if (c === "%") {
this.buf += c;
this.state = State.PASSTHROUGH;
} else {
this.handleFormat();
}
break;
default:
throw Error("Should be unreachable, certainly a bug in the lib.");
}
}
// check for unhandled args
let extras = false;
let err = "%!(EXTRA";
for (let i = 0; i !== this.haveSeen.length; ++i) {
if (!this.haveSeen[i]) {
extras = true;
err += ` '${Deno.inspect(this.args[i])}'`;
}
}
err += ")";
if (extras) {
this.buf += err;
}
return this.buf;
}
// %[<positional>]<flag>...<verb>
handleFormat(): void {
this.flags = new Flags();
const flags = this.flags;
for (; this.i < this.format.length; ++this.i) {
const c = this.format[this.i];
switch (this.state) {
case State.PERCENT:
switch (c) {
case "[":
this.handlePositional();
this.state = State.POSITIONAL;
break;
case "+":
flags.plus = true;
break;
case "<":
flags.lessthan = true;
break;
case "-":
flags.dash = true;
flags.zero = false; // only left pad zeros, dash takes precedence
break;
case "#":
flags.sharp = true;
break;
case " ":
flags.space = true;
break;
case "0":
// only left pad zeros, dash takes precedence
flags.zero = !flags.dash;
break;
default:
if (("1" <= c && c <= "9") || c === "." || c === "*") {
if (c === ".") {
this.flags.precision = 0;
this.state = State.PRECISION;
this.i++;
} else {
this.state = State.WIDTH;
}
this.handleWidthAndPrecision(flags);
} else {
this.handleVerb();
return; // always end in verb
}
} // switch c
break;
case State.POSITIONAL: // either a verb or * only verb for now, TODO
if (c === "*") {
const worp =
this.flags.precision === -1 ? WorP.WIDTH : WorP.PRECISION;
this.handleWidthOrPrecisionRef(worp);
this.state = State.PERCENT;
break;
} else {
this.handleVerb();
return; // always end in verb
}
default:
throw new Error(`Should not be here ${this.state}, library bug!`);
} // switch state
}
}
handleWidthOrPrecisionRef(wOrP: WorP): void {
if (this.argNum >= this.args.length) {
// handle Positional should have already taken care of it...
return;
}
const arg = this.args[this.argNum];
this.haveSeen[this.argNum] = true;
if (typeof arg === "number") {
switch (wOrP) {
case WorP.WIDTH:
this.flags.width = arg;
break;
default:
this.flags.precision = arg;
}
} else {
const tmp = wOrP === WorP.WIDTH ? "WIDTH" : "PREC";
this.tmpError = `%!(BAD ${tmp} '${this.args[this.argNum]}')`;
}
this.argNum++;
}
handleWidthAndPrecision(flags: Flags): void {
const fmt = this.format;
for (; this.i !== this.format.length; ++this.i) {
const c = fmt[this.i];
switch (this.state) {
case State.WIDTH:
switch (c) {
case ".":
// initialize precision, %9.f -> precision=0
this.flags.precision = 0;
this.state = State.PRECISION;
break;
case "*":
this.handleWidthOrPrecisionRef(WorP.WIDTH);
// force . or flag at this point
break;
default:
const val = parseInt(c);
// most likely parseInt does something stupid that makes
// it unusuable for this scenario ...
// if we encounter a non (number|*|.) we're done with prec & wid
if (isNaN(val)) {
this.i--;
this.state = State.PERCENT;
return;
}
flags.width = flags.width == -1 ? 0 : flags.width;
flags.width *= 10;
flags.width += val;
} // switch c
break;
case State.PRECISION:
if (c === "*") {
this.handleWidthOrPrecisionRef(WorP.PRECISION);
break;
}
const val = parseInt(c);
if (isNaN(val)) {
// one too far, rewind
this.i--;
this.state = State.PERCENT;
return;
}
flags.precision *= 10;
flags.precision += val;
break;
default:
throw new Error("can't be here. bug.");
} // switch state
}
}
handlePositional(): void {
if (this.format[this.i] !== "[") {
// sanity only
throw new Error("Can't happen? Bug.");
}
let positional = 0;
const format = this.format;
this.i++;
let err = false;
for (; this.i !== this.format.length; ++this.i) {
if (format[this.i] === "]") {
break;
}
positional *= 10;
const val = parseInt(format[this.i]);
if (isNaN(val)) {
//throw new Error(
// `invalid character in positional: ${format}[${format[this.i]}]`
//);
this.tmpError = "%!(BAD INDEX)";
err = true;
}
positional += val;
}
if (positional - 1 >= this.args.length) {
this.tmpError = "%!(BAD INDEX)";
err = true;
}
this.argNum = err ? this.argNum : positional - 1;
return;
}
handleLessThan(): string {
const arg = this.args[this.argNum];
if ((arg || {}).constructor.name !== "Array") {
throw new Error(`arg ${arg} is not an array. Todo better error handling`);
}
let str = "[ ";
for (let i = 0; i !== arg.length; ++i) {
if (i !== 0) str += ", ";
str += this._handleVerb(arg[i]);
}
return str + " ]";
}
handleVerb(): void {
const verb = this.format[this.i];
this.verb = verb;
if (this.tmpError) {
this.buf += this.tmpError;
this.tmpError = undefined;
if (this.argNum < this.haveSeen.length) {
this.haveSeen[this.argNum] = true; // keep track of used args
}
} else if (this.args.length <= this.argNum) {
this.buf += `%!(MISSING '${verb}')`;
} else {
const arg = this.args[this.argNum]; // check out of range
this.haveSeen[this.argNum] = true; // keep track of used args
if (this.flags.lessthan) {
this.buf += this.handleLessThan();
} else {
this.buf += this._handleVerb(arg);
}
}
this.argNum++; // if there is a further positional, it will reset.
this.state = State.PASSTHROUGH;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_handleVerb(arg: any): string {
switch (this.verb) {
case "t":
return this.pad(arg.toString());
break;
case "b":
return this.fmtNumber(arg as number, 2);
break;
case "c":
return this.fmtNumberCodePoint(arg as number);
break;
case "d":
return this.fmtNumber(arg as number, 10);
break;
case "o":
return this.fmtNumber(arg as number, 8);
break;
case "x":
return this.fmtHex(arg);
break;
case "X":
return this.fmtHex(arg, true);
break;
case "e":
return this.fmtFloatE(arg as number);
break;
case "E":
return this.fmtFloatE(arg as number, true);
break;
case "f":
case "F":
return this.fmtFloatF(arg as number);
break;
case "g":
return this.fmtFloatG(arg as number);
break;
case "G":
return this.fmtFloatG(arg as number, true);
break;
case "s":
return this.fmtString(arg as string);
break;
case "T":
return this.fmtString(typeof arg);
break;
case "v":
return this.fmtV(arg);
break;
case "j":
return this.fmtJ(arg);
break;
default:
return `%!(BAD VERB '${this.verb}')`;
}
}
pad(s: string): string {
const padding = this.flags.zero ? "0" : " ";
if (this.flags.dash) {
return s.padEnd(this.flags.width, padding);
}
return s.padStart(this.flags.width, padding);
}
padNum(nStr: string, neg: boolean): string {
let sign: string;
if (neg) {
sign = "-";
} else if (this.flags.plus || this.flags.space) {
sign = this.flags.plus ? "+" : " ";
} else {
sign = "";
}
const zero = this.flags.zero;
if (!zero) {
// sign comes in front of padding when padding w/ zero,
// in from of value if padding with spaces.
nStr = sign + nStr;
}
const pad = zero ? "0" : " ";
const len = zero ? this.flags.width - sign.length : this.flags.width;
if (this.flags.dash) {
nStr = nStr.padEnd(len, pad);
} else {
nStr = nStr.padStart(len, pad);
}
if (zero) {
// see above
nStr = sign + nStr;
}
return nStr;
}
fmtNumber(n: number, radix: number, upcase = false): string {
let num = Math.abs(n).toString(radix);
const prec = this.flags.precision;
if (prec !== -1) {
this.flags.zero = false;
num = n === 0 && prec === 0 ? "" : num;
while (num.length < prec) {
num = "0" + num;
}
}
let prefix = "";
if (this.flags.sharp) {
switch (radix) {
case 2:
prefix += "0b";
break;
case 8:
// don't annotate octal 0 with 0...
prefix += num.startsWith("0") ? "" : "0";
break;
case 16:
prefix += "0x";
break;
default:
throw new Error("cannot handle base: " + radix);
}
}
// don't add prefix in front of value truncated by precision=0, val=0
num = num.length === 0 ? num : prefix + num;
if (upcase) {
num = num.toUpperCase();
}
return this.padNum(num, n < 0);
}
fmtNumberCodePoint(n: number): string {
let s = "";
try {
s = String.fromCodePoint(n);
} catch (RangeError) {
s = UNICODE_REPLACEMENT_CHARACTER;
}
return this.pad(s);
}
fmtFloatSpecial(n: number): string {
// formatting of NaN and Inf are pants-on-head
// stupid and more or less arbitrary.
if (isNaN(n)) {
this.flags.zero = false;
return this.padNum("NaN", false);
}
if (n === Number.POSITIVE_INFINITY) {
this.flags.zero = false;
this.flags.plus = true;
return this.padNum("Inf", false);
}
if (n === Number.NEGATIVE_INFINITY) {
this.flags.zero = false;
return this.padNum("Inf", true);
}
return "";
}
roundFractionToPrecision(fractional: string, precision: number): string {
if (fractional.length > precision) {
fractional = "1" + fractional; // prepend a 1 in case of leading 0
let tmp = parseInt(fractional.substr(0, precision + 2)) / 10;
tmp = Math.round(tmp);
fractional = Math.floor(tmp).toString();
fractional = fractional.substr(1); // remove extra 1
} else {
while (fractional.length < precision) {
fractional += "0";
}
}
return fractional;
}
fmtFloatE(n: number, upcase = false): string {
const special = this.fmtFloatSpecial(n);
if (special !== "") {
return special;
}
const m = n.toExponential().match(FLOAT_REGEXP);
if (!m) {
throw Error("can't happen, bug");
}
let fractional = m[F.fractional];
const precision =
this.flags.precision !== -1 ? this.flags.precision : DEFAULT_PRECISION;
fractional = this.roundFractionToPrecision(fractional, precision);
let e = m[F.exponent];
// scientific notation output with exponent padded to minlen 2
e = e.length == 1 ? "0" + e : e;
const val = `${m[F.mantissa]}.${fractional}${upcase ? "E" : "e"}${
m[F.esign]
}${e}`;
return this.padNum(val, n < 0);
}
fmtFloatF(n: number): string {
const special = this.fmtFloatSpecial(n);
if (special !== "") {
return special;
}
// stupid helper that turns a number into a (potentially)
// VERY long string.
function expandNumber(n: number): string {
if (Number.isSafeInteger(n)) {
return n.toString() + ".";
}
const t = n.toExponential().split("e");
let m = t[0].replace(".", "");
const e = parseInt(t[1]);
if (e < 0) {
let nStr = "0.";
for (let i = 0; i !== Math.abs(e) - 1; ++i) {
nStr += "0";
}
return (nStr += m);
} else {
const splIdx = e + 1;
while (m.length < splIdx) {
m += "0";
}
return m.substr(0, splIdx) + "." + m.substr(splIdx);
}
}
// avoiding sign makes padding easier
const val = expandNumber(Math.abs(n)) as string;
const arr = val.split(".");
const dig = arr[0];
let fractional = arr[1];
const precision =
this.flags.precision !== -1 ? this.flags.precision : DEFAULT_PRECISION;
fractional = this.roundFractionToPrecision(fractional, precision);
return this.padNum(`${dig}.${fractional}`, n < 0);
}
fmtFloatG(n: number, upcase = false): string {
const special = this.fmtFloatSpecial(n);
if (special !== "") {
return special;
}
// The double argument representing a floating-point number shall be
// converted in the style f or e (or in the style F or E in
// the case of a G conversion specifier), depending on the
// value converted and the precision. Let P equal the
// precision if non-zero, 6 if the precision is omitted, or 1
// if the precision is zero. Then, if a conversion with style E would
// have an exponent of X:
// - If P > X>=-4, the conversion shall be with style f (or F )
// and precision P -( X+1).
// - Otherwise, the conversion shall be with style e (or E )
// and precision P -1.
// Finally, unless the '#' flag is used, any trailing zeros shall be
// removed from the fractional portion of the result and the
// decimal-point character shall be removed if there is no
// fractional portion remaining.
// A double argument representing an infinity or NaN shall be
// converted in the style of an f or F conversion specifier.
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/fprintf.html
let P =
this.flags.precision !== -1 ? this.flags.precision : DEFAULT_PRECISION;
P = P === 0 ? 1 : P;
const m = n.toExponential().match(FLOAT_REGEXP);
if (!m) {
throw Error("can't happen");
}
const X = parseInt(m[F.exponent]) * (m[F.esign] === "-" ? -1 : 1);
let nStr = "";
if (P > X && X >= -4) {
this.flags.precision = P - (X + 1);
nStr = this.fmtFloatF(n);
if (!this.flags.sharp) {
nStr = nStr.replace(/\.?0*$/, "");
}
} else {
this.flags.precision = P - 1;
nStr = this.fmtFloatE(n);
if (!this.flags.sharp) {
nStr = nStr.replace(/\.?0*e/, upcase ? "E" : "e");
}
}
return nStr;
}
fmtString(s: string): string {
if (this.flags.precision !== -1) {
s = s.substr(0, this.flags.precision);
}
return this.pad(s);
}
fmtHex(val: string | number, upper = false): string {
// allow others types ?
switch (typeof val) {
case "number":
return this.fmtNumber(val as number, 16, upper);
break;
case "string":
const sharp = this.flags.sharp && val.length !== 0;
let hex = sharp ? "0x" : "";
const prec = this.flags.precision;
const end = prec !== -1 ? min(prec, val.length) : val.length;
for (let i = 0; i !== end; ++i) {
if (i !== 0 && this.flags.space) {
hex += sharp ? " 0x" : " ";
}
// TODO: for now only taking into account the
// lower half of the codePoint, ie. as if a string
// is a list of 8bit values instead of UCS2 runes
const c = (val.charCodeAt(i) & 0xff).toString(16);
hex += c.length === 1 ? `0${c}` : c;
}
if (upper) {
hex = hex.toUpperCase();
}
return this.pad(hex);
break;
default:
throw new Error(
"currently only number and string are implemented for hex"
);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fmtV(val: any): string {
if (this.flags.sharp) {
const options =
this.flags.precision !== -1 ? { depth: this.flags.precision } : {};
return this.pad(Deno.inspect(val, options));
} else {
const p = this.flags.precision;
return p === -1 ? val.toString() : val.toString().substr(0, p);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fmtJ(val: any): string {
return JSON.stringify(val);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function sprintf(format: string, ...args: any[]): string {
const printf = new Printf(format, ...args);
return printf.doPrintf();
}

668
std/fmt/sprintf_test.ts Normal file
View file

@ -0,0 +1,668 @@
import { sprintf } from "./sprintf.ts";
import { assertEquals } from "../testing/asserts.ts";
import { test, runIfMain } from "../testing/mod.ts";
const S = sprintf;
test(function noVerb(): void {
assertEquals(sprintf("bla"), "bla");
});
test(function percent(): void {
assertEquals(sprintf("%%"), "%");
assertEquals(sprintf("!%%!"), "!%!");
assertEquals(sprintf("!%%"), "!%");
assertEquals(sprintf("%%!"), "%!");
});
test(function testBoolean(): void {
assertEquals(sprintf("%t", true), "true");
assertEquals(sprintf("%10t", true), " true");
assertEquals(sprintf("%-10t", false), "false ");
assertEquals(sprintf("%t", false), "false");
assertEquals(sprintf("bla%t", true), "blatrue");
assertEquals(sprintf("%tbla", false), "falsebla");
});
test(function testIntegerB(): void {
assertEquals(S("%b", 4), "100");
assertEquals(S("%b", -4), "-100");
assertEquals(
S("%b", 4.1),
"100.0001100110011001100110011001100110011001100110011"
);
assertEquals(
S("%b", -4.1),
"-100.0001100110011001100110011001100110011001100110011"
);
assertEquals(
S("%b", Number.MAX_SAFE_INTEGER),
"11111111111111111111111111111111111111111111111111111"
);
assertEquals(
S("%b", Number.MIN_SAFE_INTEGER),
"-11111111111111111111111111111111111111111111111111111"
);
// width
assertEquals(S("%4b", 4), " 100");
});
test(function testIntegerC(): void {
assertEquals(S("%c", 0x31), "1");
assertEquals(S("%c%b", 0x31, 1), "11");
assertEquals(S("%c", 0x1f4a9), "💩");
//width
assertEquals(S("%4c", 0x31), " 1");
});
test(function testIntegerD(): void {
assertEquals(S("%d", 4), "4");
assertEquals(S("%d", -4), "-4");
assertEquals(S("%d", Number.MAX_SAFE_INTEGER), "9007199254740991");
assertEquals(S("%d", Number.MIN_SAFE_INTEGER), "-9007199254740991");
});
test(function testIntegerO(): void {
assertEquals(S("%o", 4), "4");
assertEquals(S("%o", -4), "-4");
assertEquals(S("%o", 9), "11");
assertEquals(S("%o", -9), "-11");
assertEquals(S("%o", Number.MAX_SAFE_INTEGER), "377777777777777777");
assertEquals(S("%o", Number.MIN_SAFE_INTEGER), "-377777777777777777");
// width
assertEquals(S("%4o", 4), " 4");
});
test(function testIntegerx(): void {
assertEquals(S("%x", 4), "4");
assertEquals(S("%x", -4), "-4");
assertEquals(S("%x", 9), "9");
assertEquals(S("%x", -9), "-9");
assertEquals(S("%x", Number.MAX_SAFE_INTEGER), "1fffffffffffff");
assertEquals(S("%x", Number.MIN_SAFE_INTEGER), "-1fffffffffffff");
// width
assertEquals(S("%4x", -4), " -4");
assertEquals(S("%-4x", -4), "-4 ");
// plus
assertEquals(S("%+4x", 4), " +4");
assertEquals(S("%-+4x", 4), "+4 ");
});
test(function testIntegerX(): void {
assertEquals(S("%X", 4), "4");
assertEquals(S("%X", -4), "-4");
assertEquals(S("%X", 9), "9");
assertEquals(S("%X", -9), "-9");
assertEquals(S("%X", Number.MAX_SAFE_INTEGER), "1FFFFFFFFFFFFF");
assertEquals(S("%X", Number.MIN_SAFE_INTEGER), "-1FFFFFFFFFFFFF");
});
test(function testFloate(): void {
assertEquals(S("%e", 4), "4.000000e+00");
assertEquals(S("%e", -4), "-4.000000e+00");
assertEquals(S("%e", 4.1), "4.100000e+00");
assertEquals(S("%e", -4.1), "-4.100000e+00");
assertEquals(S("%e", Number.MAX_SAFE_INTEGER), "9.007199e+15");
assertEquals(S("%e", Number.MIN_SAFE_INTEGER), "-9.007199e+15");
});
test(function testFloatE(): void {
assertEquals(S("%E", 4), "4.000000E+00");
assertEquals(S("%E", -4), "-4.000000E+00");
assertEquals(S("%E", 4.1), "4.100000E+00");
assertEquals(S("%E", -4.1), "-4.100000E+00");
assertEquals(S("%E", Number.MAX_SAFE_INTEGER), "9.007199E+15");
assertEquals(S("%E", Number.MIN_SAFE_INTEGER), "-9.007199E+15");
assertEquals(S("%E", Number.MIN_VALUE), "5.000000E-324");
assertEquals(S("%E", Number.MAX_VALUE), "1.797693E+308");
});
test(function testFloatfF(): void {
assertEquals(S("%f", 4), "4.000000");
assertEquals(S("%F", 4), "4.000000");
assertEquals(S("%f", -4), "-4.000000");
assertEquals(S("%F", -4), "-4.000000");
assertEquals(S("%f", 4.1), "4.100000");
assertEquals(S("%F", 4.1), "4.100000");
assertEquals(S("%f", -4.1), "-4.100000");
assertEquals(S("%F", -4.1), "-4.100000");
assertEquals(S("%f", Number.MAX_SAFE_INTEGER), "9007199254740991.000000");
assertEquals(S("%F", Number.MAX_SAFE_INTEGER), "9007199254740991.000000");
assertEquals(S("%f", Number.MIN_SAFE_INTEGER), "-9007199254740991.000000");
assertEquals(S("%F", Number.MIN_SAFE_INTEGER), "-9007199254740991.000000");
assertEquals(S("%f", Number.MIN_VALUE), "0.000000");
assertEquals(
S("%.324f", Number.MIN_VALUE),
// eslint-disable-next-line max-len
"0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005"
);
assertEquals(S("%F", Number.MIN_VALUE), "0.000000");
assertEquals(
S("%f", Number.MAX_VALUE),
// eslint-disable-next-line max-len
"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.000000"
);
assertEquals(
S("%F", Number.MAX_VALUE),
// eslint-disable-next-line max-len
"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.000000"
);
});
test(function testString(): void {
assertEquals(S("%s World%s", "Hello", "!"), "Hello World!");
});
test(function testHex(): void {
assertEquals(S("%x", "123"), "313233");
assertEquals(S("%x", "n"), "6e");
});
test(function testHeX(): void {
assertEquals(S("%X", "123"), "313233");
assertEquals(S("%X", "n"), "6E");
});
test(function testType(): void {
assertEquals(S("%T", new Date()), "object");
assertEquals(S("%T", 123), "number");
assertEquals(S("%T", "123"), "string");
assertEquals(S("%.3T", "123"), "str");
});
test(function testPositional(): void {
assertEquals(S("%[1]d%[2]d", 1, 2), "12");
assertEquals(S("%[2]d%[1]d", 1, 2), "21");
});
test(function testSharp(): void {
assertEquals(S("%#x", "123"), "0x313233");
assertEquals(S("%#X", "123"), "0X313233");
assertEquals(S("%#x", 123), "0x7b");
assertEquals(S("%#X", 123), "0X7B");
assertEquals(S("%#o", 123), "0173");
assertEquals(S("%#b", 4), "0b100");
});
test(function testWidthAndPrecision(): void {
assertEquals(
S("%9.99d", 9),
// eslint-disable-next-line max-len
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"
);
assertEquals(S("%1.12d", 9), "000000000009");
assertEquals(S("%2s", "a"), " a");
assertEquals(S("%2d", 1), " 1");
assertEquals(S("%#4x", 1), " 0x1");
assertEquals(
S("%*.99d", 9, 9),
// eslint-disable-next-line max-len
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"
);
assertEquals(
S("%9.*d", 99, 9),
// eslint-disable-next-line max-len
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"
);
assertEquals(S("%*s", 2, "a"), " a");
assertEquals(S("%*d", 2, 1), " 1");
assertEquals(S("%#*x", 4, 1), " 0x1");
});
test(function testDash(): void {
assertEquals(S("%-2s", "a"), "a ");
assertEquals(S("%-2d", 1), "1 ");
});
test(function testPlus(): void {
assertEquals(S("%-+3d", 1), "+1 ");
assertEquals(S("%+3d", 1), " +1");
assertEquals(S("%+3d", -1), " -1");
});
test(function testSpace(): void {
assertEquals(S("% -3d", 3), " 3 ");
});
test(function testZero(): void {
assertEquals(S("%04s", "a"), "000a");
});
// relevant test cases from fmt_test.go
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tests: Array<[string, any, string]> = [
["%d", 12345, "12345"],
["%v", 12345, "12345"],
["%t", true, "true"],
// basic string
["%s", "abc", "abc"],
// ["%q", "abc", `"abc"`], // TODO: need %q?
["%x", "abc", "616263"],
["%x", "\xff\xf0\x0f\xff", "fff00fff"],
["%X", "\xff\xf0\x0f\xff", "FFF00FFF"],
["%x", "", ""],
["% x", "", ""],
["%#x", "", ""],
["%# x", "", ""],
["%x", "xyz", "78797a"],
["%X", "xyz", "78797A"],
["% x", "xyz", "78 79 7a"],
["% X", "xyz", "78 79 7A"],
["%#x", "xyz", "0x78797a"],
["%#X", "xyz", "0X78797A"],
["%# x", "xyz", "0x78 0x79 0x7a"],
["%# X", "xyz", "0X78 0X79 0X7A"],
// basic bytes : TODO special handling for Buffer? other std types?
// escaped strings : TODO decide whether to have %q
// characters
["%c", "x".charCodeAt(0), "x"],
["%c", 0xe4, "ä"],
["%c", 0x672c, "本"],
["%c", "日".charCodeAt(0), "日"],
// Specifying precision should have no effect.
["%.0c", "⌘".charCodeAt(0), "⌘"],
["%3c", "⌘".charCodeAt(0), " ⌘"],
["%-3c", "⌘".charCodeAt(0), "⌘ "],
// Runes that are not printable.
// {"%c", '\U00000e00', "\u0e00"}, // TODO check if \U escape exists in js
//["%c", '\U0010ffff'.codePointAt(0), "\U0010ffff"],
// Runes that are not valid.
["%c", -1, "<22>"],
// TODO surrogate half, doesn't make sense in itself, how
// to determine in JS?
// ["%c", 0xDC80, "<22>"],
["%c", 0x110000, "<22>"],
["%c", 0xfffffffff, "<22>"],
// TODO
// escaped characters
// Runes that are not printable.
// Runes that are not valid.
// width
["%5s", "abc", " abc"],
["%2s", "\u263a", " ☺"],
["%-5s", "abc", "abc "],
["%05s", "abc", "00abc"],
["%5s", "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"],
["%.5s", "abcdefghijklmnopqrstuvwxyz", "abcde"],
["%.0s", "日本語日本語", ""],
["%.5s", "日本語日本語", "日本語日本"],
["%.10s", "日本語日本語", "日本語日本語"],
// ["%08q", "abc", `000"abc"`], // TODO verb q
// ["%-8q", "abc", `"abc" `],
//["%.5q", "abcdefghijklmnopqrstuvwxyz", `"abcde"`],
["%.5x", "abcdefghijklmnopqrstuvwxyz", "6162636465"],
//["%.3q", "日本語日本語", `"日本語"`],
//["%.1q", "日本語", `"日"`]
// change of go testcase utf-8([日]) = 0xe697a5, utf-16= 65e5 and
// our %x takes lower byte of string "%.1x", "日本語", "e6"],,
["%.1x", "日本語", "e5"],
//["%10.1q", "日本語日本語", ` "日"`],
// ["%10v", null, " <nil>"], // TODO null, undefined ...
// ["%-10v", null, "<nil> "],
// integers
["%d", 12345, "12345"],
["%d", -12345, "-12345"],
// ["%d", ^uint8(0), "255"],
//["%d", ^uint16(0), "65535"],
//["%d", ^uint32(0), "4294967295"],
//["%d", ^uint64(0), "18446744073709551615"],
["%d", -1 << 7, "-128"],
["%d", -1 << 15, "-32768"],
["%d", -1 << 31, "-2147483648"],
//["%d", (-1 << 63), "-9223372036854775808"],
["%.d", 0, ""],
["%.0d", 0, ""],
["%6.0d", 0, " "],
["%06.0d", 0, " "], // 0 flag should be ignored
["% d", 12345, " 12345"],
["%+d", 12345, "+12345"],
["%+d", -12345, "-12345"],
["%b", 7, "111"],
["%b", -6, "-110"],
// ["%b", ^uint32(0), "11111111111111111111111111111111"],
// ["%b", ^uint64(0),
// "1111111111111111111111111111111111111111111111111111111111111111"],
// ["%b", int64(-1 << 63), zeroFill("-1", 63, "")],
// 0 octal notation not allowed in struct node...
["%o", parseInt("01234", 8), "1234"],
["%#o", parseInt("01234", 8), "01234"],
// ["%o", ^uint32(0), "37777777777"],
// ["%o", ^uint64(0), "1777777777777777777777"],
["%#X", 0, "0X0"],
["%x", 0x12abcdef, "12abcdef"],
["%X", 0x12abcdef, "12ABCDEF"],
// ["%x", ^uint32(0), "ffffffff"],
// ["%X", ^uint64(0), "FFFFFFFFFFFFFFFF"],
["%.20b", 7, "00000000000000000111"],
["%10d", 12345, " 12345"],
["%10d", -12345, " -12345"],
["%+10d", 12345, " +12345"],
["%010d", 12345, "0000012345"],
["%010d", -12345, "-000012345"],
["%20.8d", 1234, " 00001234"],
["%20.8d", -1234, " -00001234"],
["%020.8d", 1234, " 00001234"],
["%020.8d", -1234, " -00001234"],
["%-20.8d", 1234, "00001234 "],
["%-20.8d", -1234, "-00001234 "],
["%-#20.8x", 0x1234abc, "0x01234abc "],
["%-#20.8X", 0x1234abc, "0X01234ABC "],
["%-#20.8o", parseInt("01234", 8), "00001234 "],
// Test correct f.intbuf overflow checks. // TODO, lazy
// unicode format // TODO, decide whether unicode verb makes sense %U
// floats
["%+.3e", 0.0, "+0.000e+00"],
["%+.3e", 1.0, "+1.000e+00"],
["%+.3f", -1.0, "-1.000"],
["%+.3F", -1.0, "-1.000"],
//["%+.3F", float32(-1.0), "-1.000"],
["%+07.2f", 1.0, "+001.00"],
["%+07.2f", -1.0, "-001.00"],
["%-07.2f", 1.0, "1.00 "],
["%-07.2f", -1.0, "-1.00 "],
["%+-07.2f", 1.0, "+1.00 "],
["%+-07.2f", -1.0, "-1.00 "],
["%-+07.2f", 1.0, "+1.00 "],
["%-+07.2f", -1.0, "-1.00 "],
["%+10.2f", +1.0, " +1.00"],
["%+10.2f", -1.0, " -1.00"],
["% .3E", -1.0, "-1.000E+00"],
["% .3e", 1.0, " 1.000e+00"],
["%+.3g", 0.0, "+0"],
["%+.3g", 1.0, "+1"],
["%+.3g", -1.0, "-1"],
["% .3g", -1.0, "-1"],
["% .3g", 1.0, " 1"],
// //["%b", float32(1.0), "8388608p-23"],
// ["%b", 1.0, "4503599627370496p-52"],
// // Test sharp flag used with floats.
["%#g", 1e-323, "1.00000e-323"],
["%#g", -1.0, "-1.00000"],
["%#g", 1.1, "1.10000"],
["%#g", 123456.0, "123456."],
//["%#g", 1234567.0, "1.234567e+06"],
// the line above is incorrect in go (according to
// my posix reading) %f-> prec = prec-1
["%#g", 1234567.0, "1.23457e+06"],
["%#g", 1230000.0, "1.23000e+06"],
["%#g", 1000000.0, "1.00000e+06"],
["%#.0f", 1.0, "1."],
["%#.0e", 1.0, "1.e+00"],
["%#.0g", 1.0, "1."],
["%#.0g", 1100000.0, "1.e+06"],
["%#.4f", 1.0, "1.0000"],
["%#.4e", 1.0, "1.0000e+00"],
["%#.4g", 1.0, "1.000"],
["%#.4g", 100000.0, "1.000e+05"],
["%#.0f", 123.0, "123."],
["%#.0e", 123.0, "1.e+02"],
["%#.0g", 123.0, "1.e+02"],
["%#.4f", 123.0, "123.0000"],
["%#.4e", 123.0, "1.2300e+02"],
["%#.4g", 123.0, "123.0"],
["%#.4g", 123000.0, "1.230e+05"],
["%#9.4g", 1.0, " 1.000"],
// The sharp flag has no effect for binary float format.
// ["%#b", 1.0, "4503599627370496p-52"], // TODO binary for floats
// Precision has no effect for binary float format.
//["%.4b", float32(1.0), "8388608p-23"], // TODO s.above
// ["%.4b", -1.0, "-4503599627370496p-52"],
// Test correct f.intbuf boundary checks.
//["%.68f", 1.0, zeroFill("1.", 68, "")], // TODO zerofill
//["%.68f", -1.0, zeroFill("-1.", 68, "")], //TODO s.a.
// float infinites and NaNs
["%f", Number.POSITIVE_INFINITY, "+Inf"],
["%.1f", Number.NEGATIVE_INFINITY, "-Inf"],
["% f", NaN, " NaN"],
["%20f", Number.POSITIVE_INFINITY, " +Inf"],
// ["% 20F", Number.POSITIVE_INFINITY, " Inf"], // TODO : wut?
["% 20e", Number.NEGATIVE_INFINITY, " -Inf"],
["%+20E", Number.NEGATIVE_INFINITY, " -Inf"],
["% +20g", Number.NEGATIVE_INFINITY, " -Inf"],
["%+-20G", Number.POSITIVE_INFINITY, "+Inf "],
["%20e", NaN, " NaN"],
["% +20E", NaN, " +NaN"],
["% -20g", NaN, " NaN "],
["%+-20G", NaN, "+NaN "],
// Zero padding does not apply to infinities and NaN.
["%+020e", Number.POSITIVE_INFINITY, " +Inf"],
["%-020f", Number.NEGATIVE_INFINITY, "-Inf "],
["%-020E", NaN, "NaN "],
// complex values // go specific
// old test/fmt_test.go
["%e", 1.0, "1.000000e+00"],
["%e", 1234.5678e3, "1.234568e+06"],
["%e", 1234.5678e-8, "1.234568e-05"],
["%e", -7.0, "-7.000000e+00"],
["%e", -1e-9, "-1.000000e-09"],
["%f", 1234.5678e3, "1234567.800000"],
["%f", 1234.5678e-8, "0.000012"],
["%f", -7.0, "-7.000000"],
["%f", -1e-9, "-0.000000"],
// ["%g", 1234.5678e3, "1.2345678e+06"],
// I believe the above test from go is incorrect according to posix, s. above.
["%g", 1234.5678e3, "1.23457e+06"],
//["%g", float32(1234.5678e3), "1.2345678e+06"],
//["%g", 1234.5678e-8, "1.2345678e-05"], // posix, see above
["%g", 1234.5678e-8, "1.23457e-05"],
["%g", -7.0, "-7"],
["%g", -1e-9, "-1e-09"],
//["%g", float32(-1e-9), "-1e-09"],
["%E", 1.0, "1.000000E+00"],
["%E", 1234.5678e3, "1.234568E+06"],
["%E", 1234.5678e-8, "1.234568E-05"],
["%E", -7.0, "-7.000000E+00"],
["%E", -1e-9, "-1.000000E-09"],
//["%G", 1234.5678e3, "1.2345678E+06"], // posix, see above
["%G", 1234.5678e3, "1.23457E+06"],
//["%G", float32(1234.5678e3), "1.2345678E+06"],
//["%G", 1234.5678e-8, "1.2345678E-05"], // posic, see above
["%G", 1234.5678e-8, "1.23457E-05"],
["%G", -7.0, "-7"],
["%G", -1e-9, "-1E-09"],
//["%G", float32(-1e-9), "-1E-09"],
["%20.5s", "qwertyuiop", " qwert"],
["%.5s", "qwertyuiop", "qwert"],
["%-20.5s", "qwertyuiop", "qwert "],
["%20c", "x".charCodeAt(0), " x"],
["%-20c", "x".charCodeAt(0), "x "],
["%20.6e", 1.2345e3, " 1.234500e+03"],
["%20.6e", 1.2345e-3, " 1.234500e-03"],
["%20e", 1.2345e3, " 1.234500e+03"],
["%20e", 1.2345e-3, " 1.234500e-03"],
["%20.8e", 1.2345e3, " 1.23450000e+03"],
["%20f", 1.23456789e3, " 1234.567890"],
["%20f", 1.23456789e-3, " 0.001235"],
["%20f", 12345678901.23456789, " 12345678901.234568"],
["%-20f", 1.23456789e3, "1234.567890 "],
["%20.8f", 1.23456789e3, " 1234.56789000"],
["%20.8f", 1.23456789e-3, " 0.00123457"],
// ["%g", 1.23456789e3, "1234.56789"],
// posix ... precision(2) = precision(def=6) - (exp(3)+1)
["%g", 1.23456789e3, "1234.57"],
// ["%g", 1.23456789e-3, "0.00123456789"], posix...
["%g", 1.23456789e-3, "0.00123457"], // see above prec6 = precdef6 - (-3+1)
//["%g", 1.23456789e20, "1.23456789e+20"],
["%g", 1.23456789e20, "1.23457e+20"],
// arrays // TODO
// slice : go specific
// TODO decide how to handle deeper types, arrays, objects
// byte arrays and slices with %b,%c,%d,%o,%U and %v
// f.space should and f.plus should not have an effect with %v.
// f.space and f.plus should have an effect with %d.
// Padding with byte slices.
// Same for strings
["%2x", "", " "], // 103
["%#2x", "", " "],
["% 02x", "", "00"],
["%# 02x", "", "00"],
["%-2x", "", " "],
["%-02x", "", " "],
["%8x", "\xab", " ab"],
["% 8x", "\xab", " ab"],
["%#8x", "\xab", " 0xab"],
["%# 8x", "\xab", " 0xab"],
["%08x", "\xab", "000000ab"],
["% 08x", "\xab", "000000ab"],
["%#08x", "\xab", "00000xab"],
["%# 08x", "\xab", "00000xab"],
["%10x", "\xab\xcd", " abcd"],
["% 10x", "\xab\xcd", " ab cd"],
["%#10x", "\xab\xcd", " 0xabcd"],
["%# 10x", "\xab\xcd", " 0xab 0xcd"],
["%010x", "\xab\xcd", "000000abcd"],
["% 010x", "\xab\xcd", "00000ab cd"],
["%#010x", "\xab\xcd", "00000xabcd"],
["%# 010x", "\xab\xcd", "00xab 0xcd"],
["%-10X", "\xab", "AB "],
["% -010X", "\xab", "AB "],
["%#-10X", "\xab\xcd", "0XABCD "],
["%# -010X", "\xab\xcd", "0XAB 0XCD "],
// renamings
// Formatter
// GoStringer
// %T TODO possibly %#T object(constructor)
["%T", {}, "object"],
["%T", 1, "number"],
["%T", "", "string"],
["%T", undefined, "undefined"],
["%T", null, "object"],
["%T", S, "function"],
["%T", true, "boolean"],
["%T", Symbol(), "symbol"],
// %p with pointers
// erroneous things
// {"", nil, "%!(EXTRA <nil>)"},
// {"", 2, "%!(EXTRA int=2)"},
// {"no args", "hello", "no args%!(EXTRA string=hello)"},
// {"%s %", "hello", "hello %!(NOVERB)"},
// {"%s %.2", "hello", "hello %!(NOVERB)"},
// {"%017091901790959340919092959340919017929593813360", 0,
// "%!(NOVERB)%!(EXTRA int=0)"},
// {"%184467440737095516170v", 0, "%!(NOVERB)%!(EXTRA int=0)"},
// // Extra argument errors should format without flags set.
// {"%010.2", "12345", "%!(NOVERB)%!(EXTRA string=12345)"},
//
// // Test that maps with non-reflexive keys print all keys and values.
// {"%v", map[float64]int{NaN: 1, NaN: 1}, "map[NaN:1 NaN:1]"},
// more floats
["%.2f", 1.0, "1.00"],
["%.2f", -1.0, "-1.00"],
["% .2f", 1.0, " 1.00"],
["% .2f", -1.0, "-1.00"],
["%+.2f", 1.0, "+1.00"],
["%+.2f", -1.0, "-1.00"],
["%7.2f", 1.0, " 1.00"],
["%7.2f", -1.0, " -1.00"],
["% 7.2f", 1.0, " 1.00"],
["% 7.2f", -1.0, " -1.00"],
["%+7.2f", 1.0, " +1.00"],
["%+7.2f", -1.0, " -1.00"],
["% +7.2f", 1.0, " +1.00"],
["% +7.2f", -1.0, " -1.00"],
["%07.2f", 1.0, "0001.00"],
["%07.2f", -1.0, "-001.00"],
["% 07.2f", 1.0, " 001.00"], //153 here
["% 07.2f", -1.0, "-001.00"],
["%+07.2f", 1.0, "+001.00"],
["%+07.2f", -1.0, "-001.00"],
["% +07.2f", 1.0, "+001.00"],
["% +07.2f", -1.0, "-001.00"]
];
test(function testThorough(): void {
tests.forEach((t, i): void => {
// p(t)
const is = S(t[0], t[1]);
const should = t[2];
assertEquals(
is,
should,
`failed case[${i}] : is >${is}< should >${should}<`
);
});
});
test(function testWeirdos(): void {
assertEquals(S("%.d", 9), "9");
assertEquals(
S("dec[%d]=%d hex[%[1]d]=%#x oct[%[1]d]=%#o %s", 1, 255, "Third"),
"dec[1]=255 hex[1]=0xff oct[1]=0377 Third"
);
});
test(function formatV(): void {
const a = { a: { a: { a: { a: { a: { a: { a: {} } } } } } } };
assertEquals(S("%v", a), "[object Object]");
assertEquals(S("%#v", a), "{ a: { a: { a: { a: [Object] } } } }");
assertEquals(
S("%#.8v", a),
"{ a: { a: { a: { a: { a: { a: { a: {} } } } } } } }"
);
assertEquals(S("%#.1v", a), "{ a: [Object] }");
});
test(function formatJ(): void {
const a = { a: { a: { a: { a: { a: { a: { a: {} } } } } } } };
assertEquals(S("%j", a), `{"a":{"a":{"a":{"a":{"a":{"a":{"a":{}}}}}}}}`);
});
test(function flagLessThan(): void {
const a = { a: { a: { a: { a: { a: { a: { a: {} } } } } } } };
const aArray = [a, a, a];
assertEquals(
S("%<#.1v", aArray),
"[ { a: [Object] }, { a: [Object] }, { a: [Object] } ]"
);
const fArray = [1.2345, 0.98765, 123456789.5678];
assertEquals(S("%<.2f", fArray), "[ 1.23, 0.99, 123456789.57 ]");
});
test(function testErrors(): void {
// wrong type : TODO strict mode ...
//assertEquals(S("%f", "not a number"), "%!(BADTYPE flag=f type=string)")
assertEquals(S("A %h", ""), "A %!(BAD VERB 'h')");
assertEquals(S("%J", ""), "%!(BAD VERB 'J')");
assertEquals(S("bla%J", ""), "bla%!(BAD VERB 'J')");
assertEquals(S("%Jbla", ""), "%!(BAD VERB 'J')bla");
assertEquals(S("%d"), "%!(MISSING 'd')");
assertEquals(S("%d %d", 1), "1 %!(MISSING 'd')");
assertEquals(S("%d %f A", 1), "1 %!(MISSING 'f') A");
assertEquals(S("%*.2f", "a", 1.1), "%!(BAD WIDTH 'a')");
assertEquals(S("%.*f", "a", 1.1), "%!(BAD PREC 'a')");
assertEquals(S("%.[2]*f", 1.23, "p"), "%!(BAD PREC 'p')%!(EXTRA '1.23')");
assertEquals(S("%.[2]*[1]f Yippie!", 1.23, "p"), "%!(BAD PREC 'p') Yippie!");
assertEquals(S("%[1]*.2f", "a", "p"), "%!(BAD WIDTH 'a')");
assertEquals(S("A", "a", "p"), "A%!(EXTRA 'a' 'p')");
assertEquals(S("%[2]s %[2]s", "a", "p"), "p p%!(EXTRA 'a')");
// remains to be determined how to handle bad indices ...
// (realistically) the entire error handling is still up for grabs.
assertEquals(S("%[hallo]s %d %d %d", 1, 2, 3, 4), "%!(BAD INDEX) 2 3 4");
assertEquals(S("%[5]s", 1, 2, 3, 4), "%!(BAD INDEX)%!(EXTRA '2' '3' '4')");
assertEquals(S("%[5]f"), "%!(BAD INDEX)");
assertEquals(S("%.[5]f"), "%!(BAD INDEX)");
assertEquals(S("%.[5]*f"), "%!(BAD INDEX)");
});
runIfMain(import.meta);

220
std/fs/README.md Normal file
View file

@ -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
```

261
std/fs/copy.ts Normal file
View file

@ -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);
}
}

553
std/fs/copy_test.ts Normal file
View file

@ -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());
}
);

48
std/fs/empty_dir.ts Normal file
View file

@ -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 });
}
}
}

125
std/fs/empty_dir_test.ts Normal file
View file

@ -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 });
}
});

49
std/fs/ensure_dir.ts Normal file
View file

@ -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);
}
}

107
std/fs/ensure_dir_test.ts Normal file
View file

@ -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 });
});

64
std/fs/ensure_file.ts Normal file
View file

@ -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());
}
}

107
std/fs/ensure_file_test.ts Normal file
View file

@ -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 });
});

53
std/fs/ensure_link.ts Normal file
View file

@ -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);
}

174
std/fs/ensure_link_test.ts Normal file
View file

@ -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 });
});

59
std/fs/ensure_symlink.ts Normal file
View file

@ -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);
}

View file

@ -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 });
});

31
std/fs/eol.ts Normal file
View file

@ -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);
}

55
std/fs/eol_test.ts Normal file
View file

@ -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);
}
});

22
std/fs/exists.ts Normal file
View file

@ -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;
}
}

48
std/fs/exists_test.ts Normal file
View file

@ -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);
});

361
std/fs/glob.ts Normal file
View file

@ -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;
}

374
std/fs/glob_test.ts Normal file
View file

@ -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);

326
std/fs/globrex.ts Normal file
View file

@ -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;
}

823
std/fs/globrex_test.ts Normal file
View file

@ -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
);
}
});

17
std/fs/mod.ts Normal file
View file

@ -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";

59
std/fs/move.ts Normal file
View file

@ -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);
}
}

330
std/fs/move_test.ts Normal file
View file

@ -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 });
});

3
std/fs/path.ts Normal file
View file

@ -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";

7
std/fs/path/README.md Normal file
View file

@ -0,0 +1,7 @@
# Deno Path Manipulation Libraries
Usage:
```ts
import * as path from "https://deno.land/std/fs/path.ts";
```

View file

@ -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");
});

54
std/fs/path/constants.ts Normal file
View file

@ -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 ? /[\\/]+/ : /\/+/;

View file

@ -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"), ".");
});

View file

@ -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.\\\\"), ".");
});

27
std/fs/path/interface.ts Normal file
View file

@ -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>;

View file

@ -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);
});

128
std/fs/path/join_test.ts Normal file
View file

@ -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]);
});
});

25
std/fs/path/mod.ts Normal file
View file

@ -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;

View file

@ -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);
});
});

422
std/fs/path/posix.ts Normal file
View file

@ -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;
}

View file

@ -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);
});
});

Some files were not shown because too many files have changed in this diff Show more