scandir.py: a fast, greppable file index instead of Windows search

Let’s be honest: Windows search sucks 😉 You type a file name you know exists, watch the green bar crawl through the Explorer window, and end up with either nothing, the wrong thing, or a web search suggestion you never asked for. The search index covers some folders but not others, it silently skips network drives and external disks, and it has its own idea of what is relevant. When all you want is “where on earth did I put budget_forecast_2019.xlsx ?”, that is just frustrating.

So I went back to an old-school approach: create a plain text file that lists every file of a directory tree once, together with its date and size, and then search that file with grep. Searching a text file with a million lines takes well under a second, works with regular expressions, can be combined with other command-line tools, and gives you exactly what you asked for, nothing more.

Years ago I simply used dir /s > all.txt for that. It works, but the output splits path and file name across lines (so a grep hit on a file name does not tell you where the file is), the format depends on the system language, and special characters in file names turn into codepage garbage. Hence the small Python tool below.

What scandir.py does

scandir.py walks a directory tree and writes one line per file and folder into a text file (default: all.txt). Each line contains the modification date, the size (or <DIR> for folders) and the full path, so every line is self-contained and can be grepped on its own.

  • Fast: it uses os.scandir(), which on Windows gets size and date directly from the directory listing without extra disk I/O. On my machine it indexes about 1.1 million files in 44 seconds.
  • Same order as dir /s: entries are sorted case-insensitively, folders and files mixed, and the tree is processed depth-first, with a blank line after each directory listing.
  • Robust: output is UTF-8; odd file names are written with replacement characters instead of crashing, and folders without access permission are skipped silently.
  • Progress bar: it counts the folders on the first two levels beforehand and shows a progress bar on stderr while scanning.
  • No dependencies: it needs only the Python standard library; runs on Windows, Linux and macOS.

Usage:

python scandir.py                    # index the current folder into all.txt
python scandir.py C:\Users\wilfried  # index a specific folder
python scandir.py D:\ -o d_drive.txt # index a whole drive into d_drive.txt

Run it every now and then (e.g., via the Windows Task Scheduler) to keep the index reasonably current. Since it is just a snapshot, files created after the last run will of course not show up.

Source code

# This script creates a searchable textfile of all files in the file tree.
# The output filename default is all.txt, but it can be changed with the -o option
#
# Takes 44 seconds to index 1.1 million files
#
# 2026 Wil

import os
import sys
import time

ROOT = "."
OUTFILE = "all.txt"
BLOCK_SIZE = 100_000  # lines per write

# Cache formatted timestamps per minute (many files share the same minute)
_date_cache = {}
_strftime = time.strftime
_localtime = time.localtime


def fmt_date(mtime):
    key = int(mtime) // 60
    s = _date_cache.get(key)
    if s is None:
        s = _strftime("%d.%m.%Y  %H:%M", _localtime(key * 60))
        if len(_date_cache) > 200_000:
            _date_cache.clear()
        _date_cache[key] = s
    return s


def sort_key(entry):
    return entry.name.upper()


def count_top_dirs(root):
    """Count the folders on levels 1 and 2 below root (used as progress steps)."""
    total = 0
    try:
        with os.scandir(root) as it:
            level1 = [e.path for e in it if e.is_dir(follow_symlinks=False)]
    except OSError:
        return 0
    total += len(level1)
    for d in level1:
        try:
            with os.scandir(d) as it:
                total += sum(1 for e in it if e.is_dir(follow_symlinks=False))
        except OSError:
            pass
    return total


BAR_W = 40


def show_progress(done, total, state):
    # Redraws only when the whole-percent value changes: at most 101 writes per run
    pct = 100 if total <= 0 else min(100, done * 100 // total)
    if pct != state[0]:
        state[0] = pct
        filled = BAR_W * pct // 100
        sys.stderr.write(f"\r[{'â–ˆ' * filled}{'â–‘' * (BAR_W - filled)}] {pct:3d}%")
        sys.stderr.flush()


def main():
    root = ROOT
    outfile = OUTFILE
    args = sys.argv[1:]
    i = 0
    while i < len(args):
        arg = args[i]
        low = arg.lower()
        if low in ("-o", "--o", "-out", "--out"):
            i += 1
            if i >= len(args):
                sys.exit("missing file name after " + arg)
            outfile = args[i]
        else:
            root = arg
        i += 1
    root = os.path.abspath(root)
    try:
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")  # bar chars on any console codepage
    except (AttributeError, ValueError):
        pass
    total = count_top_dirs(root)
    done = 0
    pstate = [-1]
    show_progress(0, total, pstate)
    stack = [(root, 0)]
    buf = []
    append = buf.append
    scandir = os.scandir

    # utf-8 + errors="replace": no codepage garbage, no crash on odd file names
    with open(outfile, "w", encoding="utf-8", errors="replace", newline="\n") as f:
        write = f.write
        while stack:
            current, depth = stack.pop()
            if 0 < depth <= 2:
                done += 1
                show_progress(done, total, pstate)
            try:
                it = scandir(current)
            except OSError:
                continue
            with it:
                entries = list(it)
            # Same order as `dir /s`: case-insensitive by name, folders and files mixed
            entries.sort(key=sort_key)
            subdirs = []
            start = len(buf)
            for entry in entries:
                try:
                    # On Windows, stat() is served from the directory listing: no extra I/O
                    if entry.is_dir(follow_symlinks=False):
                        st = entry.stat(follow_symlinks=False)
                        append(f"{fmt_date(st.st_mtime)}\t<DIR>\t{entry.path}")
                        subdirs.append((entry.path, depth + 1))
                    else:
                        st = entry.stat(follow_symlinks=False)
                        append(f"{fmt_date(st.st_mtime)}\t{st.st_size}\t{entry.path}")
                except OSError:
                    continue
            if len(buf) > start:
                append("")  # blank line after each directory's listing, like dir /s
            # reversed, so the stack pops subfolders in sorted order (depth-first like dir /s)
            stack.extend(reversed(subdirs))
            if len(buf) >= BLOCK_SIZE:
                write("\n".join(buf) + "\n")
                buf.clear()
        if buf:
            write("\n".join(buf) + "\n")
    show_progress(total, total, pstate)
    sys.stderr.write("\n")


if __name__ == "__main__":
    main()

Output format

Here is an excerpt of an index generated for a sample directory:

14.03.2018  09:14	<DIR>	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12
02.11.2021  14:52	<DIR>	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\infrastructure_70
02.11.2021  14:52	<DIR>	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\q3_reports_29
19.07.2016  22:05	4388	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\scandir.py
05.05.2023  08:33	1455569	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\strategy_draft_271.logx

28.09.2019  17:41	<DIR>	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\architecture_41
28.09.2019  17:41	4520241	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\budget_forecast_791.cfgx
11.01.2015  11:20	<DIR>	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\client_assets_51
11.01.2015  11:20	4452759	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\client_feedback_415.cfgx
30.06.2025  03:12	2897609	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\employee_matrix_527.secx
30.06.2025  03:12	<DIR>	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\financials_18
12.12.2017  19:50	7358195	C:\Users\exampleuser\Documents\webpage\tech.nes.aau.at\sampledir\architecture_12\network_topology_537.arch

Each line has three fields separated by a tab character, date/time, size in bytes (or <DIR> for a folder) and the filename with the full path.

Searching with grep

On Linux and macOS, grep is already there; on Windows you get it with Git for Windows (Git Bash), WSL, or a native port. Some examples:

Find all files with a certain name part (case-insensitive):

grep -i "budget_forecast" all.txt
28.09.2019  17:41	4520241	C:\...\sampledir\architecture_12\budget_forecast_791.cfgx
12.12.2017  19:50	1393438	C:\...\sampledir\architecture_12\architecture_41\budget_forecast_744.arch
17.10.2024  16:29	6027244	C:\...\sampledir\architecture_12\client_assets_51\budget_forecast_971.secx

Find all files with a given extension (the $ anchors the match at the end of the line):

grep -i "\.cfgx$" all.txt

Find only folders whose name contains research_data:

grep "<DIR>" all.txt | grep -i "research_data"

Find everything modified in 2026:

grep -E "^[0-9]{2}\.[0-9]{2}\.2026" all.txt

Find files larger than 7 MB (here awk uses the tab-separated size column):

awk -F'\t' '$2 ~ /^[0-9]+$/ && $2 > 7000000' all.txt

Show only the paths of the hits, without date and size:

grep -i "seminar" all.txt | cut -f3

If you don’t have grep at hand, Windows’ built-in findstr also does the job for simple searches: findstr /i "budget_forecast" all.txt. But given the fact that grep is a much more powerful tool, I wamly recommend adding it to your system.

Happy searching!

Merge Password-Protected PDF files

Sometimes, PDF files—like those from book chapters—come password-protected to prevent editing, which also stops you from merging them without knowing the password. If printing is allowed, you can work around this by printing the file to PDF, but this creates a new PDF where the text is no longer selectable or searchable. While you could re-OCR the file, a much better solution is to use an old yet powerful tool: Ghostscript, originally developed by Adobe.

Ghostscript allows you to manipulate PDF files easily, including merging multiple PDFs into one. For example, to merge three PDFs named 1.pdf, 2.pdf, and 3.pdf, you can run the following command:

gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=merged.pdf 1.pdf 2.pdf 3.pdf

Here’s what the parameters mean:-dBATCH: Tells Ghostscript to process all files and then exit (batch mode).-dNOPAUSE: Disables pauses between processing each page (no manual intervention required).-q: Runs the command quietly, suppressing most of the output messages.-sDEVICE=pdfwrite: Specifies that the output should be written as a PDF.-sOutputFile=merged.pdf: Defines the name of the resulting merged file.

If you have many files that are consecutively named (e.g., 1.pdf to 100.pdf), you can generate the file list dynamically in the command line, like this:

gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=merged.pdf $(seq -f "%g.pdf" 1 100)

Note that the gs tool takes considerable time to handle so many entries, but this approach saves your time after all.

Limit upload / download speed of Dropbox and Seafile

Cloud storage app like Dropbox and Seafile can eat a lot of your bandwidth, which is especially painful when you are on a slow WiFi connection.

Luckily, these apps come with a possibility to limit their bandwidth use.

For Dropbox,

  • click on the Dropbox icon in the system tray
  • click the cogwheel in the right corner
  • select “Preferences”
  • go to the bandwidth tab and enter the limits

For Seafile,

  • right-click on the Seafile icon in the system tray
  • Select “Preferences”
  • enter the limits on the general tab

Schedule meetings automatically into Google Calendar with Doodle or Calendly

doodle.com is a well known page for scheduling meetings or making decisions. Its calendar integration is probably less known. Doodle can export a calendar with the dates you have chosen in order to remind you to keep them blocked. When a date is fixed for a query, doodle releases the other dates. You can also add your calendar to doodle to see your availabilities right in the doodle calendar and to use such features as autofill.

If you look for something else, calendly.com is another free service that provides an interface for others to schedule a meeting with you. Dates with concurring events in your Google calendar are automatically blanked out.

 

Use TortoiseSVN without Passwort

Tortoise SVN has an option for saving authentication, but it does not work for SVN. In order to save the authentication, go to Tortoise settings, for example by right clicking in the Windows Explorer and then selecting Tortoise > Settings. In the settings window, select Network. Then add the path to Tortoise Plink (it is in the “bin” folder of the Tortoise installation) and add an option for a username and password using options -l and -pw.

For example:
“C:\Program Files\TortoiseSVN\bin\TortoisePlink.exe” -l foo -pw bar

 

filegive: send and receive files across computers

filegive easily sends or receives files point-to-point, with authentication and ciphering, and the other side only needs a Web browser. No third party server is involved in the transfer. It can use common NAT traversal protocols like uPnP and NAT-PMP, manually forwarded ports, or a public ssh server.

filegive is a commandline tool available for Linux, Windows and Mac OS.

Project page with commands and download links