如何在Deno中使用文件系统

226 阅读4分钟

Working with the File System in Deno

在这篇文章中,我们将在介绍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文件系统工作