In this article, we’ll build on our introduction to Deno by creating a command-line tool that can search for text within files and folders. We’ll use a range of API methods that Deno provides to read and write to the file system.
In our last installment, we used Deno to build a command-line tool to make requests to a third-party API. In this article, we’re going to leave the network to one side and build a tool that lets you search the file system for text in files and folders within your current directory — similar to tools like grep
.
Note: we’re not building a tool that will be as optimized and efficient as grep
, nor are we aiming to replace it! The aim of building a tool like this is to get familiar with Deno’s file system APIs.
Installing Deno
Table of Contents
We’re going to assume that you’ve got Deno up and running on your machine locally. You can check the Deno website or the previous article for more detailed installation instructions and also to get information on how to add Deno support to your editor of choice.
At the time of writing, the latest stable version of Deno is 1.10.2, so that’s what I’m using in this article.
Setting Up Our New Command with Yargs
As in the previous article, we’ll use Yargs to build the interface that our users can use to execute our tool. Let’s create index.ts
and populate it with the following:
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);
There’s a fair bit going on here that’s worth pointing out:
- We install Yargs by pointing to its path on the Deno repository. I explicitly use a precise version number to make sure we always get that version, so that we don’t end up using whatever happens to be the latest version when the script runs.
- At the time of writing, the Deno + TypeScript experience for Yargs isn’t great, so I’ve created my own interface and used that to provide some type safety.
UserArguments
contains all the inputs we’ll ask the user for. For now, we’re only going to ask fortext
, but in future we could expand this to provide a list of files to search for, rather than assuming the current directory.
We can run this with deno run index.ts
and see our Yargs output:
$ 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
Now it’s time to get implementing!
Listing Files
Before we can start searching for text in a given file, we need to generate a list of directories and files to search within. Deno provides Deno.readdir, which is part of the “built-ins” library, meaning you don’t have to import it. It’s available for you on the global namespace.
Deno.readdir
is asynchronous and returns a list of files and folders in the current directory. It returns these items as an AsyncIterator, which means we have to use the for await … of loop to get at the results:
for await (const fileOrFolder of Deno.readDir(Deno.cwd())) { console.log(fileOrFolder); }
This code will read from the current working directory (which Deno.cwd()
gives us) and log each result. However, if you try to run the script now, you’ll get an error:
$ 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
Remember that Deno requires all scripts to be explicitly given permissions to read from the file system. In our case, the --allow-read
flag will enable our code to run:
~/$ 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 }
In this case, I’m running the script in the directory where I’m building our tool, so it finds the TS source code, the .git
repository and the .vscode
folder. Let’s start writing some functions to recursively navigate this structure, as we need to find all the files within the directory, not just the top level ones. Additionally, we can add some common ignores. I don’t think anyone will want the script to search the entire .git
folder!
In the code below, we’ve created the getFilesList
function, which takes a directory and returns all files in that directory. If it encounters a directory, it will recursively call itself to find any nested files, and return the result:
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; }
We can then use this like so:
const files = await getFilesList(Deno.cwd()); console.log(files);
We also get some output that looks good:
$ 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" ]
Using the path
Module
We’re could now combine file paths with template strings like so:
`${directory}/${fileOrFolder.name}`,
But it would be nicer to do this using Deno’s path
module. This module is one of the modules that Deno provides as part of its standard library (much like Node does with its path
module), and if you’ve used Node’s path
module the code will look very similar. At the time of writing, the latest version of the std
library Deno provides is 0.97.0
, and we import the path
module from the mod.ts
file:
import * as path from "https://deno.land/std@0.97.0/path/mod.ts";
mod.ts
is always the entrypoint when importing Deno’s standard modules. The documentation for this module lives on the Deno site and lists path.join
, which will take multiple paths and join them into one path. Let’s import and use that function rather than manually combining them:
// 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; }
When using the standard library, it’s vital that you remember to pin to a specific version. Without doing so, your code will always load the latest version, even if that contains changes that will break your code. The Deno docs on the standard library go into this further, and I recommend giving that page a read.
Continue reading Working with the File System in Deno on SitePoint.