DEV Community

I.D.
I.D.

Posted on

I made a CLI tool to search "TODO"s and distributed on Homebrew

Introduction

In this post, I will write about the story I made a CLI tool and distributed it, also serves as note to self.
This post includes following contents:

  • A brief explanation of Rust
  • Notes on how to distribute your tools via homebrew

The tool I made

TodoScanner

A fast, friendly tool to scan your codebase for TODOs, FIXMEs, and other actionable comments. Built in Rust for speed and reliability.

Features

  • Scan directories recursively for annotated comments (e.g., TODO, FIXME, NOTE)
  • Supports common source file extensions (Rust, JS/TS, Python, etc.)
  • Configurable markers and ignore patterns
  • Clean, readable output with file paths and line numbers
  • Exit codes suitable for CI workflows

Installation

brew tap I-Dieod/tap
brew install tds
Enter fullscreen mode Exit fullscreen mode

Usage

# Scan current directory
tds

# Scan a specific path with default markers
tds path/to/project
Enter fullscreen mode Exit fullscreen mode

Example Output

Roadmap

  • Markdown/HTML report output
  • Interactive TUI

Contributing

Issues and PRs are welcome. Please open an issue with a clear description and minimal repro if you find a bug.

License

MIT License. See LICENSE for details.

Acknowledgments

Inspired by the need to keep TODOs visible and actionable during daily development and CI.




Target readers

  • Someone who wants to distribute their own tools
  • Someone who wants to know how to read files and extract comments using Rust

Background

These days I was programming a logic to extract comment lines(Like "TODO:", "FIX:" or something else) for Zed extension, similar to TodoTree.
However I realized it would be more difficult than I thought, and a similar extension already existed. Another reason was that zed_extension_api couldn't interact Zed's UI from extension. So I froze that project indefinitely, and I thought, "Why not distribute the byproduct logic via Homebrew?"
There is already a leading project doing this, but I thought "My logic might run faster because it's made with Rust" and "the leading project stopped updating 4 years ago." You know what, I just decided to do it anyway.

How does the tool work?

Actually it's simple logic.
The args value receives arguments for command, at the same time, the tool will get current directory the tool is running by env library, and declares to scan from current directory if args is empty.

let args: Vec<String> = env::args().collect();
let cdu: String = env::current_dir().unwrap().to_string_lossy().to_string();
let root_path = if args.len() > 1 { &args[1] } else { &cdu };

println!("🔍 Scanning for COMMENT items in: {}", root_path);
Enter fullscreen mode Exit fullscreen mode

The tool determine if it's directory or not, by is_dir() method in Path Object, and scan files recursively.
path.extension() checks the file extension.

if path.is_dir() {
            scan_directory(&path, todos)?;
        } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
            // Check if it's a supported file type
            match ext {
                "rs" | "js" | "ts" | "jsx" | "tsx" | "py" | "java" | "go" | "c" | "cpp" | "h"
                | "hpp" | "php" | "rb" | "swift" | "kt" | "scala" | "html" | "css" | "vue"
                | "md" | "txt" | "yaml" | "yml" | "json" | "toml" | "xml" | "sh" | "bash"
                | "zsh" => {
                    scan_file(&path, todos)?;
                }
                _ => {}
            }
        }
Enter fullscreen mode Exit fullscreen mode

Whan the tool finds a file with a matching extension, it reads the file contents using the scan_file function.
Then it extracts lines start with TODO:, FIX:, NOTE:, etc. from comment lines beginning with //, #, or other comment markers, and adds them to a struct.
After scanning all directories, the tool declares a BTreeMap object to sort items by comment type, and pushes items into it one by one.
Finally, it iterates through a predefined order array using a for loop, and prints the content of each item grouped by comment type using println!.

for comment_type in &type_order {
            if let Some(files) = by_type.get(*comment_type) {
                println!("🏷️  {} Items:", comment_type);
                println!("   {}", "=".repeat(comment_type.len() + 7));

                for (file, file_todos) in files {
                    println!("    📁 {}", file);
                    for todo in file_todos {
                        println!("        📌 Line {}: {}", todo.line + 1, todo.text);
                    }
                    println!();
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

Distribute via Homebrew

To distribute the tool via Homebrew, I created a new repository named "homebrew-tap", and put a Formula file written by Ruby into the repo.




At this time, make sure that the package name defined in Cargo.toml, the Formula file name, and the class name inside it are all the same.

If they don't match, you'll get an error when running brew tap, which I'll mention later.

I left the coding of the Formula file to Claude Sonnet 4.5.

# !This class name!
class Tds < Formula
  desc "Fast TODO comment scanner for your codebase"
  homepage "https://github.com/I-Dieod/TodoScanner"
  url "https://github.com/I-Dieod/TodoScanner/archive/refs/tags/v0.2.0.tar.gz"
  sha256 "f2afe11e469036e8af3344e546d85df70fbe6340241c156309b6b904f520befa"
  license "MIT"

  depends_on "rust" => :build

  def install
    system "cargo", "install", *std_cargo_args
  end

  test do
    # Create a test file with a TODO comment
    (testpath/"test.rs").write <<~EOS
      // TODO: Test comment
      fn main() {}
    EOS

    output = shell_output("#{bin}/tds #{testpath}")
    assert_match "Found 1 TODO items", output
  end
end
Enter fullscreen mode Exit fullscreen mode

For the actual content, you need to prepare the tarball link you can get from your repository releases and SHA256 hash you can get from that tarball link.
You need to write them in url and sha256 fields.
If you're using Nushell or Bash shells, you can get the hash by running the following command:

curl -L [tarball_url] | shasum -a 256
Enter fullscreen mode Exit fullscreen mode

Treatment of hashes, and URLs

The hash is validation information, not a secret.
In fact, by publishing it, users can verify that the downloaded file hasn't been tampered with.
This is a security-enhancing mechanism.

The thing you should never publish

On the other hand, you must never include the following things in your Fomula.
❌ API keys, tokens
❌ Password
❌ Access tokens of private repository
❌ Secret keys

After you push the Formula file you created to the remote repository, run these commands in your CLI:

brew tap [your_user_name]/tap
brew install [your_user_name]/[package_name_you_want]
# or
brew install [your_user_name]/tap/[package_name_you_want]
Enter fullscreen mode Exit fullscreen mode

Now you can use the tool you made in your CLI via Homebrew.

The output when my tool working

How to update brew packages

Basically, you only have to replace url and sha256 in Formula, to newest version.

Final Thoughts

I really wanted to make Zed extension, but I feel frustrated that it already existed. My idea was beaten.
I just did it anyway, thinking I should start creating tools and contributing OSS to OSS, taking this as an opportunity.
I'm grad if this post helps someone who wants to distribute something via Homebrew.
I'm always open to complaints about my tool, PRs on GitHub, or corrections if I said something wrong. Also, I'm Japanese, and I'm writing this post in English as much as I can by myself. If you can't understand some parts, feel free to ask questions in the comments.
Thank you for reading!

Top comments (0)