Getting Started
Introducing PICLE
Command-line interfaces are great when you want a fast workflow: type a command, get output, repeat. In practice, a good interactive shell also needs help, completion, and input validation.
PICLE provides those features by turning Pydantic v2 models into an interactive shell. You define a model tree, and PICLE interprets the command line as a walk through that tree.
At a high level, your shell looks like this:
Root
├─ command -> SubModel
│ ├─ arg -> Field (collect value)
│ ├─ flag -> Field (presence)
│ └─ <ENTER> -> run() / field function
└─ command -> Field (collect value)
Concretely: field names (or alias / serialization_alias) become command tokens.
Tokens after a leaf field become that field’s value(s). Once PICLE has enough information,
it validates the collected data with Pydantic and then executes your code.
How a line is processed:
user input
-> parse tokens into (models + fields)
-> collect values (supports quotes, JSON-ish {..} / [..], multiline)
-> validate with Pydantic
-> execute (model.run or field function)
-> processors / outputter
-> print
A small shell
This creates:
Root
└─ show
├─ version (function)
└─ clock (function)
import time
from typing import Any
from pydantic import BaseModel, Field
from picle import App
class Show(BaseModel):
version: Any = Field(
None,
description="Show software version",
json_schema_extra={"function": "show_version"},
)
clock: Any = Field(
None,
description="Show current clock",
json_schema_extra={"function": "show_clock"},
)
@staticmethod
def show_version():
return "0.1.0"
@staticmethod
def show_clock():
return time.ctime()
class Root(BaseModel):
show: Show = Field(None, description="Show commands")
class PicleConfig:
prompt = "picle#"
intro = "PICLE Sample app"
if __name__ == "__main__":
App(Root).start()
Try it:
picle#show version
0.1.0
picle#show clock
Fri May 2 22:44:01 2025
What gets executed
PICLE decides what to call in this order:
1) If the current model has a run(**kwargs) method, it calls run.
2) Otherwise, if the last referenced field has json_schema_extra={"function": "..."}, PICLE calls that model staticmethod.
That keeps simple “command -> function” shells small, while still supporting bigger models that centralize behavior in run().
Help and discovery
PICLE’s help is model-driven:
picle#?
... shows available top-level commands and built-in commands
picle#show ?
... shows available fields under "show"
picle#show version ?
... shows what ENTER will do
For a command tree view:
picle#man tree
For a JSON schema (useful if you want to expose your shell as documented data):
picle#man json-schema
Subshells (optional)
If a model sets PicleConfig.subshell = True, you can “enter” that model as a subshell.
When you navigate to the model without providing arguments, PICLE pushes it onto a shell stack and updates the prompt.
Shell navigation commands:
exit leave current subshell
top return to the root shell
end exit the app
pwd print current shell path
cls clear the screen
Pipes (optional)
If the current model declares PicleConfig.pipe, the | token starts a new command segment.
The second segment receives the previous segment’s result as its first argument.
segment 0 (produce data)
| segment 1 (transform/format)
| segment 2 (transform/format)
PICLE includes a ready-to-use pipe model (PipeFunctionsModel) with filters and outputters.
Configuration shells with ConfigModel
If you want a CLI that edits a structured YAML configuration, PICLE includes picle.models.ConfigModel.
It lets users navigate a nested Pydantic model, stage edits into <config_file>.tmp, review diffs, and then commit.
Minimal shape:
from pydantic import BaseModel, Field
from picle import App
from picle.models import ConfigModel, PipeFunctionsModel
class Logging(BaseModel):
severity: str = Field(None, description="Log severity")
class MyConfig(ConfigModel):
logging: Logging = Field(None, description="Logging config")
class PicleConfig:
subshell = True
prompt = "app[cfg]#"
config_file = "app_config.yaml"
class Root(BaseModel):
configure: MyConfig = Field(None, description="Edit configuration")
class PicleConfig:
prompt = "app#"
pipe = PipeFunctionsModel
if __name__ == "__main__":
App(Root).start()
Example interaction:
app#configure
app[cfg]#logging severity debug
Configuration updated (uncommitted). Use 'commit' to save or 'show changes' to review.
app[cfg]#show changes
... unified diff ...
app[cfg]#commit
Configuration committed successfully
ConfigModel also honors a additional PicleConfig parameters:
config_file: YAML config path (default:configuration.yaml)backup_on_save: number of rotating backups to keep on commit (default: 5)commit_hook: optional callable executed after a successful commit
Notes:
show configurationprints the running config,show changesprints the staged diff.clear-changesdiscards the staged temp file;erase-configurationstages an empty config.rollback <n>loadsapp_config.yaml.old<n>into the temp file for review/commit.
Multi-line input
If a field sets json_schema_extra={"multiline": True}, the user can type input as the value to start multi-line collection.
PICLE reads lines until EOF (Ctrl+D), then joins them with newlines and validates the result.
Sample shell apps
If you want copy/paste starting points, the repository includes a couple of small runnable examples:
docs_sample_app_1.py: a tiny “show version/clock” shellconfig_app_example.py: a completeConfigModelconfiguration shell example
Search for those filenames in the repository to find the runnable source files.