在这篇文章中,我们将在介绍Deno的基础上,创建一个可以在文件和文件夹中搜索文本的命令行工具。我们将使用Deno提供的一系列API方法来读取和写入文件系统。
在上一篇文章中,我们用Deno建立了一个命令行工具来向第三方API发出请求。在这篇文章中,我们将把网络放在一边,建立一个工具,让你在文件系统中搜索当前目录下的文件和文件夹中的文本--类似于grep 。
注意:我们建立的工具不会像grep 那样优化和高效,我们的目标也不是要取代它!建立这样一个工具的目的是为了熟悉Deno的文件系统API。
安装Deno
我们将假设你已经在你的机器上安装并运行了Deno。你可以查看Deno网站或之前的文章,以获得更详细的安装说明,也可以获得关于如何将Deno支持添加到你选择的编辑器中的信息。
在写这篇文章的时候,Deno的最新稳定版本是1.10.2,所以这就是我在这篇文章中使用的。
用Yargs设置我们的新命令
和上一篇文章一样,我们将使用Yargs来建立我们的用户可以用来执行我们的工具的界面。让我们创建index.ts ,并在其中加入以下内容。
import yargs from "https://deno.land/x/yargs@v17.0.1-deno/deno.ts";
interface Yargs<ArgvReturnType> {
describe: (param: string, description: string) => Yargs<ArgvReturnType>;
demandOption: (required: string[]) => Yargs<ArgvReturnType>;
argv: ArgvReturnType;
}
interface UserArguments {
text: string;
}
const userArguments: UserArguments =
(yargs(Deno.args) as unknown as Yargs<UserArguments>)
.describe("text", "the text to search for within the current directory")
.demandOption(["text"])
.argv;
console.log(userArguments);
这里有相当多的事情值得指出。
- 我们通过指向Deno资源库的路径来安装Yargs。我明确地使用了一个精确的版本号,以确保我们总是得到这个版本,这样我们就不会在脚本运行时使用任何刚好是最新的版本。
- 在写这篇文章的时候,Deno+TypeScript对Yargs的体验并不好,所以我创建了自己的接口,用它来提供一些类型安全。
UserArguments包含所有我们将要求用户的输入。现在,我们只要求 ,但在将来,我们可以扩展到提供一个要搜索的文件列表,而不是假设当前目录。text
我们可以用deno run index.ts ,看看我们的Yargs输出。
$ deno run index.ts
Check file:///home/jack/git/deno-file-search/index.ts
Options:
--help Show help [boolean]
--version Show version number [boolean]
--text the text to search for within the current directory [required]
Missing required argument: text
现在,是时候开始实施了!
列出文件
在我们开始搜索某个文件中的文本之前,我们需要生成一个要搜索的目录和文件的列表。Deno提供了 Deno.readdir,它是 "内置 "库的一部分,意味着你不必导入它。它可以在全局命名空间中为你所用。
Deno.readdir 是异步的,返回当前目录下的文件和文件夹的列表。它将这些项目作为一个 AsyncIterator,这意味着我们必须使用 for await ... of循环来获取结果。
for await (const fileOrFolder of Deno.readDir(Deno.cwd())) {
console.log(fileOrFolder);
}
这段代码将从当前工作目录(Deno.cwd() 给我们的)中读取,并记录每个结果。然而,如果你现在尝试运行这个脚本,你会得到一个错误。
$ deno run index.ts --text='foo'
error: Uncaught PermissionDenied: Requires read access to <CWD>, run again with the --allow-read flag
for await (const fileOrFolder of Deno.readDir(Deno.cwd())) {
^
at deno:core/core.js:86:46
at unwrapOpResult (deno:core/core.js:106:13)
at Object.opSync (deno:core/core.js:120:12)
at Object.cwd (deno:runtime/js/30_fs.js:57:17)
at file:///home/jack/git/deno-file-search/index.ts:19:52
记住,Deno要求所有的脚本都要有明确的权限才能从文件系统中读取。在我们的例子中,--allow-read 标志将使我们的代码能够运行。
~/$ deno run --allow-read index.ts --text='foo'
{ name: ".git", isFile: false, isDirectory: true, isSymlink: false }
{ name: ".vscode", isFile: false, isDirectory: true, isSymlink: false }
{ name: "index.ts", isFile: true, isDirectory: false, isSymlink: false }
在这种情况下,我在构建我们工具的目录中运行脚本,所以它找到了TS源代码、.git 仓库和.vscode 文件夹。让我们开始编写一些函数来递归浏览这个结构,因为我们需要找到目录内的所有文件,而不仅仅是最上面的文件。此外,我们还可以添加一些常用的忽略项。我不认为有人会希望脚本搜索整个.git 文件夹!
在下面的代码中,我们创建了getFilesList 函数,它接收一个目录并返回该目录中的所有文件。如果它遇到一个目录,它将递归地调用自己,找到任何嵌套的文件,并返回结果。
const IGNORED_DIRECTORIES = new Set([".git"]);
async function getFilesList(
directory: string,
): Promise<string[]> {
const foundFiles: string[] = [];
for await (const fileOrFolder of Deno.readDir(directory)) {
if (fileOrFolder.isDirectory) {
if (IGNORED_DIRECTORIES.has(fileOrFolder.name)) {
// Skip this folder, it's in the ignore list.
continue;
}
// If it's not ignored, recurse and search this folder for files.
const nestedFiles = await getFilesList(
`${directory}/${fileOrFolder.name}`,
);
foundFiles.push(...nestedFiles);
} else {
// We found a file, so store it.
foundFiles.push(`${directory}/${fileOrFolder.name}`);
}
}
return foundFiles;
}
然后我们可以像这样使用它。
const files = await getFilesList(Deno.cwd());
console.log(files);
我们还得到一些看起来不错的输出。
$ deno run --allow-read index.ts --text='foo'
[
"/home/jack/git/deno-file-search/.vscode/settings.json",
"/home/jack/git/deno-file-search/index.ts"
]
使用path 模块
我们现在可以像这样把文件路径和模板字符串结合起来。
`${directory}/${fileOrFolder.name}`,
但如果使用Deno的path 模块来做这件事就更好了。这个模块是Deno作为其标准库的一部分提供的模块之一(很像Node的path 模块),如果你使用过Node的path 模块,代码看起来会非常相似。在撰写本文时,Deno提供的std 库的最新版本是0.97.0 ,我们从mod.ts 文件中导入path 模块。
import * as path from "https://deno.land/std@0.97.0/path/mod.ts";
mod.ts 是导入Deno的标准模块时的入口。这个模块的文档住在Deno网站上,并列出了 ,它将采取多个路径并将它们连接成一个路径。让我们导入并使用该函数,而不是手动组合它们。path.join
// import added to the top of our script
import yargs from "https://deno.land/x/yargs@v17.0.1-deno/deno.ts";
import * as path from "https://deno.land/std@0.97.0/path/mod.ts";
// update our usages of the function:
async function getFilesList(
directory: string,
): Promise<string[]> {
const foundFiles: string[] = [];
for await (const fileOrFolder of Deno.readDir(directory)) {
if (fileOrFolder.isDirectory) {
if (IGNORED_DIRECTORIES.has(fileOrFolder.name)) {
// Skip this folder, it's in the ignore list.
continue;
}
// If it's not ignored, recurse and search this folder for files.
const nestedFiles = await getFilesList(
path.join(directory, fileOrFolder.name),
);
foundFiles.push(...nestedFiles);
} else {
// We found a file, so store it.
foundFiles.push(path.join(directory, fileOrFolder.name));
}
}
return foundFiles;
}
当使用标准库时,你必须记住将其固定在一个特定的版本上,这一点至关重要。如果不这样做,你的代码将总是加载最新的版本,即使它包含的变化会破坏你的代码。Deno关于标准库的文档对此有进一步的阐述,我建议阅读该页面。
继续阅读SitePoint上的Deno文件系统工作。