Create a CLI Tool With fastcore.script

by Audrey M. Roy Greenfeld | Wed, Feb 5, 2025

Fastcore's call_parse decorator turns Python functions into CLI tools quickly. It's nowhere near as fancy as Typer or Click, but it's super quick to use.


Study the Example

From https://fastcore.fast.ai/script.html#example

#| export
from fastcore.script import *
from fastcore.utils import *
from pathlib import Path
import time
from nbdev.export import nb_export
@call_parse
def main(msg:str,     # The message
         upper:bool): # Convert to uppercase?
    "Print `msg`, optionally converting to uppercase"
    print(msg.upper() if upper else msg)

We can use the function right here, which is nice:

main("hey", upper=True)
main("hey", upper=False)

As for using it as a CLI tool, the docs say "If you copy that info a file and run it, you’ll see:


$ examples/test_fastcore.py --help
usage: test_fastcore.py [-h] [--upper] msg

Print `msg`, optionally converting to uppercase

positional arguments:
  msg          The message

optional arguments:
  -h, --help   show this help message and exit
  --upper      Convert to uppercase? (default: False)

"

Real-World Example: Prepending Notebook Filenames With Dates

I have many notebooks like Docker-Run.ipynb that I'd like prepended with the ISO 8601 date that they were last updated. My script will be prepend_nbs_with_dates.py because of the next line:

#| default_exp prepend_nbs_with_dates

I'll define a function to get all of them:

#| export
def get_undated_nbs(): return L(Path().glob('[!0-9]*.ipynb'))
nbs = get_undated_nbs()
nbs
nbs[1]

Then I'll define a function to prepend a date to a filename:

#| export
def prepend_date(fn):
    "Prepend ISO 8601 date to filename if not already present"
    if fn.stem[0].isdigit(): return  # Already has date
    mtime = fn.stat().st_mtime
    date = time.strftime('%Y-%m-%d', time.localtime(mtime))
    new_name = fn.parent/f"{date}-{fn.name}"
    fn.rename(new_name)
    return new_name
# prepend_date(nbs[1])

Finally, I'll define a main function that gets all undated notebooks and prepends dates to their filenames:

#| export
@call_parse
def main(dry_run:bool=True): # Don't actually rename if True
    "Prepend dates to notebook filenames"
    fns = get_undated_nbs()
    print(f"Found {len(fns)} undated notebooks")
    if dry_run:
        for f in fns:
            mtime = f.stat().st_mtime
            date = time.strftime('%Y-%m-%d', time.localtime(mtime))
            print(f"{f} -> {date}-{f.name}")
    else:
        for f in fns: prepend_date(f)

By default this does a dry run:

main()

Here we run it for real:

main(dry_run=False)

Now there are no more undated notebooks:

get_undated_nbs()

Export It

nb_export("2025-02-05-Create-a-CLI-Tool-With-Fastcore-Script.ipynb", lib_path="../scripts")
!ls ../scripts/

Run It

I ran it as a function earlier, so I don't have undated notebooks at the moment!

% python ../scripts/prepend_nbs_with_dates.py
Found 0 undated notebooks

Give me a few hours or days, and I'll update this post...