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:
commit
28293acd9c
264 changed files with 68231 additions and 0 deletions
50
std/README.md
Normal file
50
std/README.md
Normal 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
488
std/archive/tar.ts
Normal 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
91
std/archive/tar_test.ts
Normal 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
1
std/archive/testdata/example.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hello world!
|
16
std/bundle/README.md
Normal file
16
std/bundle/README.md
Normal 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
11
std/bundle/run.ts
Normal 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
116
std/bundle/test.ts
Normal 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
67
std/bundle/testdata/bundle.js
vendored
Normal 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
107
std/bundle/utils.ts
Normal 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
96
std/bytes/mod.ts
Normal 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
74
std/bytes/test.ts
Normal 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
37
std/datetime/README.md
Normal 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
146
std/datetime/mod.ts
Normal 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
96
std/datetime/test.ts
Normal 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
219
std/encoding/README.md
Normal 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
251
std/encoding/csv.ts
Normal 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
592
std/encoding/csv_test.ts
Normal 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
143
std/encoding/hex.ts
Normal 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
182
std/encoding/hex_test.ts
Normal 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
3
std/encoding/testdata/CRLF.toml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[boolean]
|
||||
bool1 = true
|
||||
bool2 = false
|
12
std/encoding/testdata/arrayTable.toml
vendored
Normal file
12
std/encoding/testdata/arrayTable.toml
vendored
Normal 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
8
std/encoding/testdata/arrays.toml
vendored
Normal 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
3
std/encoding/testdata/boolean.toml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[boolean] # i hate comments
|
||||
bool1 = true
|
||||
bool2 = false
|
56
std/encoding/testdata/cargo.toml
vendored
Normal file
56
std/encoding/testdata/cargo.toml
vendored
Normal 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
147
std/encoding/testdata/cargoTest.toml
vendored
Normal 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
8
std/encoding/testdata/datetime.toml
vendored
Normal 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
23
std/encoding/testdata/float.toml
vendored
Normal 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
|
7
std/encoding/testdata/inlineTable.toml
vendored
Normal file
7
std/encoding/testdata/inlineTable.toml
vendored
Normal 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
20
std/encoding/testdata/integer.toml
vendored
Normal 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
5
std/encoding/testdata/simple.toml
vendored
Normal 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
30
std/encoding/testdata/string.toml
vendored
Normal 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
13
std/encoding/testdata/table.toml
vendored
Normal 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
565
std/encoding/toml.ts
Normal 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
410
std/encoding/toml_test.ts
Normal 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
47
std/examples/README.md
Normal 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
10
std/examples/cat.ts
Normal 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
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
4
std/examples/colors.ts
Normal 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!")))));
|
9
std/examples/echo_server.ts
Normal file
9
std/examples/echo_server.ts
Normal 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
65
std/examples/gist.ts
Executable 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
29
std/examples/test.ts
Normal 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
72
std/flags/README.md
Normal 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
34
std/flags/all_bool_test.ts
Executable 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
168
std/flags/bool_test.ts
Executable 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
29
std/flags/dash_test.ts
Executable 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
33
std/flags/default_bool_test.ts
Executable 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
24
std/flags/dotted_test.ts
Executable 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
5
std/flags/example.ts
Normal 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
14
std/flags/kv_short_test.ts
Executable 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
20
std/flags/long_test.ts
Executable 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
317
std/flags/mod.ts
Normal 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
41
std/flags/num_test.ts
Executable 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
201
std/flags/parse_test.ts
Executable 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
46
std/flags/short_test.ts
Executable 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
16
std/flags/stop_early_test.ts
Executable 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
98
std/flags/unknown_test.ts
Executable 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
8
std/flags/whitespace_test.ts
Executable 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
212
std/fmt/README.md
Normal 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
12
std/fmt/TODO
Normal 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
148
std/fmt/colors.ts
Normal 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
121
std/fmt/colors_test.ts
Normal 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"), "[31mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function doubleColor(): void {
|
||||
assertEquals(c.bgBlue(c.red("foo bar")), "[44m[31mfoo bar[39m[49m");
|
||||
});
|
||||
|
||||
test(function replacesCloseCharacters(): void {
|
||||
assertEquals(c.red("Hel[39mlo"), "[31mHel[31mlo[39m");
|
||||
});
|
||||
|
||||
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"), "[31mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testBold(): void {
|
||||
assertEquals(c.bold("foo bar"), "[1mfoo bar[22m");
|
||||
});
|
||||
|
||||
test(function testDim(): void {
|
||||
assertEquals(c.dim("foo bar"), "[2mfoo bar[22m");
|
||||
});
|
||||
|
||||
test(function testItalic(): void {
|
||||
assertEquals(c.italic("foo bar"), "[3mfoo bar[23m");
|
||||
});
|
||||
|
||||
test(function testUnderline(): void {
|
||||
assertEquals(c.underline("foo bar"), "[4mfoo bar[24m");
|
||||
});
|
||||
|
||||
test(function testInverse(): void {
|
||||
assertEquals(c.inverse("foo bar"), "[7mfoo bar[27m");
|
||||
});
|
||||
|
||||
test(function testHidden(): void {
|
||||
assertEquals(c.hidden("foo bar"), "[8mfoo bar[28m");
|
||||
});
|
||||
|
||||
test(function testStrikethrough(): void {
|
||||
assertEquals(c.strikethrough("foo bar"), "[9mfoo bar[29m");
|
||||
});
|
||||
|
||||
test(function testBlack(): void {
|
||||
assertEquals(c.black("foo bar"), "[30mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testRed(): void {
|
||||
assertEquals(c.red("foo bar"), "[31mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testGreen(): void {
|
||||
assertEquals(c.green("foo bar"), "[32mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testYellow(): void {
|
||||
assertEquals(c.yellow("foo bar"), "[33mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testBlue(): void {
|
||||
assertEquals(c.blue("foo bar"), "[34mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testMagenta(): void {
|
||||
assertEquals(c.magenta("foo bar"), "[35mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testCyan(): void {
|
||||
assertEquals(c.cyan("foo bar"), "[36mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testWhite(): void {
|
||||
assertEquals(c.white("foo bar"), "[37mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testGray(): void {
|
||||
assertEquals(c.gray("foo bar"), "[90mfoo bar[39m");
|
||||
});
|
||||
|
||||
test(function testBgBlack(): void {
|
||||
assertEquals(c.bgBlack("foo bar"), "[40mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgRed(): void {
|
||||
assertEquals(c.bgRed("foo bar"), "[41mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgGreen(): void {
|
||||
assertEquals(c.bgGreen("foo bar"), "[42mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgYellow(): void {
|
||||
assertEquals(c.bgYellow("foo bar"), "[43mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgBlue(): void {
|
||||
assertEquals(c.bgBlue("foo bar"), "[44mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgMagenta(): void {
|
||||
assertEquals(c.bgMagenta("foo bar"), "[45mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgCyan(): void {
|
||||
assertEquals(c.bgCyan("foo bar"), "[46mfoo bar[49m");
|
||||
});
|
||||
|
||||
test(function testBgWhite(): void {
|
||||
assertEquals(c.bgWhite("foo bar"), "[47mfoo bar[49m");
|
||||
});
|
674
std/fmt/sprintf.ts
Normal file
674
std/fmt/sprintf.ts
Normal 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
668
std/fmt/sprintf_test.ts
Normal 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
220
std/fs/README.md
Normal 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
261
std/fs/copy.ts
Normal 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
553
std/fs/copy_test.ts
Normal 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
48
std/fs/empty_dir.ts
Normal 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
125
std/fs/empty_dir_test.ts
Normal 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
49
std/fs/ensure_dir.ts
Normal 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
107
std/fs/ensure_dir_test.ts
Normal 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
64
std/fs/ensure_file.ts
Normal 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
107
std/fs/ensure_file_test.ts
Normal 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
53
std/fs/ensure_link.ts
Normal 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
174
std/fs/ensure_link_test.ts
Normal 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
59
std/fs/ensure_symlink.ts
Normal 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);
|
||||
}
|
169
std/fs/ensure_symlink_test.ts
Normal file
169
std/fs/ensure_symlink_test.ts
Normal 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
31
std/fs/eol.ts
Normal 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
55
std/fs/eol_test.ts
Normal 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
22
std/fs/exists.ts
Normal 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
48
std/fs/exists_test.ts
Normal 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
361
std/fs/glob.ts
Normal 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
374
std/fs/glob_test.ts
Normal 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
326
std/fs/globrex.ts
Normal 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
823
std/fs/globrex_test.ts
Normal 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
17
std/fs/mod.ts
Normal 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
59
std/fs/move.ts
Normal 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
330
std/fs/move_test.ts
Normal 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
3
std/fs/path.ts
Normal 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
7
std/fs/path/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Deno Path Manipulation Libraries
|
||||
|
||||
Usage:
|
||||
|
||||
```ts
|
||||
import * as path from "https://deno.land/std/fs/path.ts";
|
||||
```
|
76
std/fs/path/basename_test.ts
Normal file
76
std/fs/path/basename_test.ts
Normal 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
54
std/fs/path/constants.ts
Normal 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 ? /[\\/]+/ : /\/+/;
|
62
std/fs/path/dirname_test.ts
Normal file
62
std/fs/path/dirname_test.ts
Normal 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"), ".");
|
||||
});
|
90
std/fs/path/extname_test.ts
Normal file
90
std/fs/path/extname_test.ts
Normal 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
27
std/fs/path/interface.ts
Normal 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>;
|
34
std/fs/path/isabsolute_test.ts
Normal file
34
std/fs/path/isabsolute_test.ts
Normal 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
128
std/fs/path/join_test.ts
Normal 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
25
std/fs/path/mod.ts
Normal 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;
|
180
std/fs/path/parse_format_test.ts
Normal file
180
std/fs/path/parse_format_test.ts
Normal 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
422
std/fs/path/posix.ts
Normal 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;
|
||||
}
|
73
std/fs/path/relative_test.ts
Normal file
73
std/fs/path/relative_test.ts
Normal 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
Loading…
Reference in a new issue