Vulcano is a Python framework for building interactive command-line applications with minimal boilerplate.
Support the project on Patreon
Built on top of prompt_toolkit, Vulcano turns plain Python functions into fully featured CLI commands โ complete with autocompletion, inline help, syntax highlighting, and command history โ with no extra configuration required.
Its simplicity makes it suitable for a wide range of scenarios where you need to expose existing functions through a REPL or a one-shot argument interface.
$ python your_app.py help
๐ Available Commands
โญโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Command โ Description โ
โโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ reverse_word โ Return the word reversed. โ
โ random_upper_word โ Return the word with randomly capitalised letters. โ
โ multiply โ Multiply two numbers. โ
โ bye โ Say goodbye to someone. โ
โ help โ Print global help or details for a specific command. โ
โฐโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Note
Vulcano is under active development. The public API may change between minor versions while we work toward a stable 1.x release. For production use, pin to a specific version.
Autocomplete โ Vulcano inspects every registered function and automatically builds a completion list that includes command names and their arguments.
Inline help โ Help text is derived from docstrings or from the description provided at registration time.
History โ Use the up and down arrow keys to navigate through previous commands.
Module registration โ Register all public functions from an existing module without modifying its source.
Syntax highlighting โ Powered by Pygments and prompt_toolkit for a polished REPL experience.
Argument value options โ Attach a list of predefined choices to any argument with
arg_opts; the autocompleter offers them and quotes values that contain spaces automatically:@app.command("greet", "Greet by role", arg_opts={"role": ["admin", "user", "guest"]}) def greet(name, role="user"): return "Hello, {} {}!".format(role.capitalize(), name)
Concatenated commands โ Chain multiple commands with
and, both in argument mode and in the interactive REPL:python your_script.py my_func arg="something" and my_func_2 arg="another thing"
Context a plain dictionary available to all registered functions.
Command templating โ Use any value stored in the context to parameterise commands at runtime.
Autosuggestion โ When an unknown command is entered, Vulcano suggests the closest match:
๐ niu ๐ค Command 'niu' not found ๐ก Did you mean: "new"? ๐
Command groups โ Organise commands into named sub-contexts with
app.group(). Typing the group name in the REPL enters an isolated sub-session;exitreturns to the parent. Commands in any group can also be run directly with dot-path syntax โ both in REPL and argument modes โ without entering the sub-session at all:๐ text.hi name=Alice Hi! Mr. Alice :) Glad to see you. ๐
Groups can be nested to any depth. The prompt chains all ancestor names so the current nesting level is always visible:
๐ text ๐ text > formal ๐ text > formal > dear name=Alice Dear Dr. Alice, I trust this finds you well. ๐ text > formal > exit ๐ text > exit ๐
Source inspection โ Append
?to any command name to view its source code with syntax highlighting. Dot-path commands are supported too:๐ bye? @app.command def bye(name="User"): """ Say goodbye to someone """ return "Bye {}!".format(name) ๐
Install the latest release from PyPI:
pip install vulcanoTo install a development version directly from the repository:
git clone https://github.com/dgarana/vulcano.git
cd vulcano
pip install -e .The repository includes a complete example in examples/simple_example.py.
If you just want to see Vulcano working quickly, start there.
There is also an async/background-output example in
examples/async_output_example.py showing how Vulcano behaves when a
background task prints while the REPL prompt is still active.
Quick start ~~~~~~~~~~~n 1. Install the package:
pip install vulcano- Save the example app below as
simple_example.py(or use the version inexamples/simple_example.py). - Run it in interactive mode:
python simple_example.py- Try a few realistic commands:
๐ hi name=Alice title=Dr.
Hi! Dr. Alice โ glad to see you.
๐ i_am name=Alice
๐ whoami
Alice
๐ greet name=Alice role=admin
Hello, Admin Alice!
๐ multiply number1=6 number2=7
42
๐ text.formal.dear name=Alice title="Prof."
Dear Prof. Alice, I trust this finds you well.
- Or run one-shot commands directly from the shell:
python simple_example.py multiply number1=6 number2=7
python simple_example.py text.formal.dear name=Alice title="Prof."
python simple_example.py multiply number1=6 number2=7 and reverse_word word=vulcanoThe longer snippet below covers the most common features:
import random
from vulcano.app import VulcanoApp
from vulcano.themes import MonokaiTheme
app = VulcanoApp()
@app.command("hi", "Greet someone by name")
def salute_method_here(name, title="Mr."):
"""Greet a person.
Args:
name (str): Name of the person to greet.
title (str): Honorific title.
"""
print("Hi! {} {} โ glad to see you.".format(title, name))
def has_context_name():
"""Return True only when a name has been set in the context."""
return "name" in app.context
@app.command
def i_am(name):
"""Store your name in the context.
Args:
name (str): Your name.
"""
app.context["name"] = name
@app.command(show_if=has_context_name)
def whoami():
"""Return your name from the context.
Only shown after ``i_am`` has been called.
"""
return app.context["name"]
@app.command
def bye(name="User"):
"""Say goodbye to someone."""
return "Bye {}!".format(name)
@app.command
def sum_numbers(*args):
"""Return the sum of all provided numbers."""
return sum(args)
@app.command
def multiply(number1, number2):
"""Multiply two numbers."""
return number1 * number2
@app.command
def reverse_word(word):
"""Return the word reversed."""
return word[::-1]
@app.command
def random_upper_word(word):
"""Return the word with randomly capitalised letters."""
return "".join(random.choice([letter.upper(), letter]) for letter in word)
@app.command("greet", "Greet someone by role", arg_opts={"role": ["admin", "user", "guest"]})
def greet_by_role(name, role="user"):
"""Greet someone and mention their role.
Args:
name (str): Name of the person to greet.
role (str): Role of the person.
"""
return "Hello, {} {}!".format(role.capitalize(), name)
if __name__ == "__main__":
app.run(theme=MonokaiTheme)Groups let you organise commands into named sub-contexts, each with its
own isolated sub-REPL. Create a group with app.group() and register
commands on it the same way you would on the main app:
from vulcano.app import VulcanoApp
app = VulcanoApp()
# Create a group โ its name is what the user types to enter it.
text = app.group("text", "Text-related commands")
@text.command("hi", "Greet someone")
def say_hi(name, title="Mr."):
print("Hi! {} {}!".format(title, name))
@text.command("greet", "Greet by role", arg_opts={"role": ["admin", "user"]})
def greet_by_role(name, role="user"):
return "Hello, {} {}!".format(role.capitalize(), name)
if __name__ == "__main__":
app.run()Typing the group name in the REPL enters the sub-session, where only
that group's commands โ plus a local help and exit โ are
available. The prompt reflects the current depth:
๐ text
๐ text > hi name=Alice
Hi! Mr. Alice!
๐ text > exit
๐
Nested groups โ Groups can contain other groups to any depth:
formal = text.group("formal", "Formal greetings")
@formal.command("dear", "Send a formal greeting")
def formal_dear(name, title="Dr."):
return "Dear {} {}, I trust this finds you well.".format(title, name)The prompt chains all ancestor names:
๐ text
๐ text > formal
๐ text > formal > dear name=Alice title=Prof.
Dear Prof. Alice, I trust this finds you well.
๐ text > formal > exit
๐ text > exit
๐
Dot-path syntax โ Run any group command directly without entering
the sub-session, using group.command notation. This works in both
REPL and argument modes and can cross multiple nesting levels:
๐ text.hi name=Alice
Hi! Mr. Alice!
๐ text.formal.dear name=Alice title="Prof."
Dear Prof. Alice, I trust this finds you well.
$ python your_app.py text.hi name=Alice
Hi! Mr. Alice!
$ python your_app.py text.formal.dear name=Alice and bye
Dear Dr. Alice, I trust this finds you well.
Bye User!Autocomplete is dot-path aware: typing text. offers text.hi,
text.greet, text.formal; typing text.formal. narrows to
text.formal.dear. Argument and arg_opts completions work the
same as for top-level commands once the full path is typed.
Vulcano ships five built-in themes, all importable from
vulcano.themes:
| Theme | Description |
|---|---|
MonokaiTheme |
Classic Monokai (default). |
DraculaTheme |
Dracula โ pink keywords, purple numbers, yellow strings. |
NordTheme |
Nord โ muted blues and greens on a dark background. |
|
|
OneDarkTheme |
Atom One Dark โ purple keywords, green strings, orange numbers. |
Pass any theme to app.run():
from vulcano.app import VulcanoApp
from vulcano.themes import DraculaTheme
app.run(theme=DraculaTheme)To create a custom theme, subclass VulcanoStyle and define a
styles dict using standard Pygments token types:
from pygments.token import Keyword, Name, Number, String, Text
from vulcano.themes import VulcanoStyle
class MyTheme(VulcanoStyle):
styles = {
Text: "#ffffff",
Keyword: "bold #ff0000",
Name: "#00ff00",
Number: "#0000ff",
String: "#ffff00",
}
app.run(theme=MyTheme)The snippet above registers the following commands:
i_am, whoami, bye, sum_numbers, multiply,
reverse_word, random_upper_word, and greet.
See Command Groups below to learn how to organise commands into
named sub-contexts.
Commands that return a value have their result printed automatically
and stored in context["last_result"], making it available for
subsequent commands via templating.
REPL mode โ launch with no arguments to start the interactive shell:
$ python simple_example.py
๐ i_am name=Alice
๐ whoami
Alice
๐ reverse_word word=vulcano
onacluv
๐ multiply number1=6 number2=7
42
๐ bye
Bye User!
๐ help command=reverse_word
โญโโโโโโโโโโโโโโโโโโโโโโโโโโ โ๏ธ reverse_word โโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Return the word reversed. โ
โ โ
โ Argument Type Default Description โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โก word str The word to reverse. โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
๐ greet name=Alice role=admin
Hello, Admin Alice!
๐ multiply number1=6 number2=7 and reverse_word word=vulcano
42
onacluv
๐ exit
๐ See you soon!
Argument mode โ pass commands directly; chain with and:
$ python simple_example.py multiply number1=6 number2=7 and reverse_word word=vulcano
42
onacluv
$ python simple_example.py reverse_word "Hello Baby! This is awesome" and random_upper_word "{last_result}"
emosewa si sihT !ybaB olleH
eMOsEwa SI SIHT !YbaB OLlEHA few behaviors are especially useful when you start building non-trivial apps:
- Commands that
returna value have that value printed automatically. - Returned values are also stored in
context["last_result"]and can be reused in later commands. print(...)is useful for side-effect-style commands, whilereturnworks better for commands whose output should be reused.- Group commands can be executed either by entering the group in the REPL or by using dot-path syntax directly.
arg_optscan be static lists or dynamic callables, which makes it possible to offer context-aware suggestions.
Vulcano stores command results in context["last_result"]. By default, inline
commands can reference context values using Python-style placeholders such as
{last_result}.
For example:
python simple_example.py reverse_word word=vulcano and random_upper_word word="{last_result}"This works because inline command parsing accepts placeholder braces and Vulcano formats arguments with the current context before execution.
If you want to disable this behavior and treat placeholders literally, you can set it either at construction time or afterwards:
app = VulcanoApp(enable_context_formatting=False)
# or later:
app.enable_context_formatting = FalseWhen disabled, values such as {last_result} are passed through as plain text
instead of being substituted from the context.
For a more realistic reference app, prefer examples/simple_example.py over
minimal one-function snippets.
If you want to start a background task that keeps writing to stdout while the
REPL remains active, take a look at examples/async_output_example.py.
It demonstrates a command that starts a daemon thread and emits periodic output
without corrupting the interactive prompt.
Example session:
๐ start_background interval=1 ticks=3
Background task started
๐ hello name=Alice
Hello Alice!
[background] tick 0
[background] tick 1
[background] tick 2
๐
The project ships a Makefile that wraps all common development tasks.
Run make help (or just make) to see the full list of targets:
make fmt # Format code with black and isort
make black # Format with black only
make isort # Sort imports with isort only
make lint # Check style with flake8
make flake8 # Run flake8 only
make security # Scan with bandit
make bandit # Run bandit only
make test # Run the test suite
make check # All checks without modifying files (CI-friendly)
make all # Format, check, and test in one stepVulcano is maintained as a community-driven project, but its day-to-day implementation work can be significantly accelerated by AI-assisted automation. In practice, community members propose ideas, fixes, and improvements through GitHub issues, and an automated AI workflow can review those requests, prepare changes, and open pull requests for human review.
This does not change the core purpose of the repository: Vulcano remains a framework for building Python command-line utilities by reusing existing Python functions and exposing them through a simpler command interface. AI-assisted contributions should remain aligned with that goal and operate within the repository rules documented in AGENTS.md when present.
Contributions of all kinds are welcome โ bug reports, feature requests, documentation improvements, and pull requests.
Before submitting a pull request, please ensure that:
- All existing tests pass (
make test). - New functionality is covered by tests.
- Code is formatted and all checks pass (
make check).
Open an issue first if you are planning a significant change, so the approach can be discussed before implementation.