Skip to content

fss-cli

fss-cli turns filesystems you already use in Python into shell-shaped commands inside your own command-line tool.

If you have an fsspec filesystem — vosfs, S3, GCS, local disk, or anything else — fss-cli gives you ls, cp, rm, du, find, cat, and more against it, without writing any of them yourself.

It is a library, not a program

fss-cli installs no fss-cli executable. It hands your application a Typer app that you mount into your own CLI. You decide which filesystems are reachable and what they are called.

Install

uv add "git+https://github.com/opencadc/fsspec-tools@main#subdirectory=libs/fss-cli"

The whole idea in one example

from contextlib import asynccontextmanager

import fsspec
import typer
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
from fss_cli import App


@asynccontextmanager
async def data_source():
    """Yield one async-capable filesystem for a single command invocation."""
    yield AsyncFileSystemWrapper(fsspec.filesystem("memory"))


app = typer.Typer()
app.add_typer(App({"data": data_source}).typer_app, name="fs")

if __name__ == "__main__":
    app()

That is a complete CLI. Every path is spelled name:/path, where name is a key from the mapping you passed:

python app.py fs ls data:/
python app.py fs cp data:/report.csv data:/backup/report.csv
python app.py fs du -sh data:/project

The name: prefix is not decoration — it is how one command reaches two different filesystems at once:

python app.py fs cp local:/results.csv archive:/2026/results.csv

With a real backend

The example above uses memory so it runs with no setup. A real host wires up the filesystems it actually serves — here local disk plus a VOSpace archive:

from contextlib import asynccontextmanager

import fsspec
import typer
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
from fss_cli import App
from vosfs import VOSpaceFileSystem


@asynccontextmanager
async def local_source():
    yield AsyncFileSystemWrapper(fsspec.filesystem("file"))


@asynccontextmanager
async def archive_source():
    fs = VOSpaceFileSystem(
        endpoint_url="https://staging.canfar.net/arc",
        asynchronous=True,
        skip_instance_cache=True,
    )
    try:
        yield fs
    finally:
        await fs.aclose()


app = typer.Typer()
app.add_typer(
    App({"local": local_source, "archive": archive_source}).typer_app,
    name="fs",
)
python app.py fs ls -lh archive:/project
python app.py fs cp local:/reduced.fits archive:/project/reduced.fits
python app.py fs du -sh archive:/project

Credentials come from the backend, not from fss-cli — vosfs picks up VOSFS_CERT_FILE or VOSFS_TOKEN_FILE as usual. See Integration for why each source is a fresh filesystem per invocation.

Where to go next

  • Integration


    Sources, lifecycle, capabilities, extensions, and exit statuses — what you need to embed this correctly.

  • Command reference


    Every command, its options, and what it actually does against a remote backend.

  • API reference


    App, AppCapabilities, CommandContext, and the rest of the public surface.

What it does not claim

fss-cli is not POSIX, GNU, or BSD compatible, and does not work with every fsspec backend. It provides a shell-compatible experience: it renders what a backend can actually supply, in the shape a shell user expects, and omits the rest rather than inventing values.

Supported host platforms are Linux and macOS. The commands and source forms with test evidence behind them are listed in the tested command matrix.