# run installation
go install github.com/pt-main/run/cmd/run@latest
# tal installation
go install github.com/pt-main/run/cmd/tal@latestrun is a tool for managing scripts, scripting any scenarios in an embedded Lua-like language with incrementality, storing scripts in global/local storage, complete independence from system and platform (works anywhere Go compiles), and with built-in ways to distribute scripts, for example via GitHub.
The project contains Task Lua (tal) inside itself - a task runner seamlessly integrated into run. More details can be read in the project README.
Russian version of this document: README-RU.md.
Changelog: docs/changelog.md | Russian version.
| Problem | run solves |
|---|---|
| Scripts scattered across projects | Global storage ~/run/ |
| Need to remember paths | One command: run -r myscript |
| Different languages | Support for Python, Bash, Batch, Lua - and easily extensible |
| Grouping | Tags for selective running (--tagged) |
| Project scripts | Local mode with .run/ in the current folder |
| Security | TYCL config with a strict contract |
| Compactness | Small binary while fully platform-independent |
run gives globality, simplicity and control without unnecessary complexity.
And why Tal?
| Problem | tal solves |
|---|---|
| Makefile is hard to read and write | Simple DSL with comments and Lua instead of Shell |
| Incrementality works poorly | SHA256 hashes instead of modification time |
| No calling tasks from each other | Tasks can be called via a built-in function |
| File dependencies are cumbersome | works out of the box |
tal gives incrementality, modernity and Lua - all in one tool.
Download the release for your OS/architecture and put it in PATH:
# Linux/macOS
chmod +x run-linux-amd64
sudo mv run-linux-amd64 /usr/local/bin/run
# Windows
# Just put run-windows-amd64.exe in a folder that is in PATHgo install github.com/pt-main/run/cmd/run@latest
go install github.com/pt-main/run/cmd/tal@latest # optional, standalone talOn first launch run will create a structure in ~/run/:
config.tycl- config with the list of scripts.scripts/- Lua wrappers for launching.templates/- bodies of custom wrapper templates.base/- original script files.
The CLI consists of the root parser run and three subcommands: manage (script and
template management), sys (system operations) and tal (the bundled task runner).
| Command | Description | Example |
|---|---|---|
-r <name> [args...] |
Run a script by name (explicit form) | run -r mypy arg1 arg2 |
<name> [args...] |
Run a script (when the name does not conflict with run commands) | run deploy --env=prod |
-r --tagged="tag1;tag2;..." |
Run all scripts carrying any of the given tags | run -r --tagged="deploy;test" |
-r --tagged="..." --parallel |
Run the tagged scripts in parallel | run -r --tagged="deploy;build" --parallel |
-r --tagged="..." --args="..." |
Pass arguments to the script (use it if arguments conflict with run flags) | run -r --tagged="deploy" --args="--tagged dev" |
-r --tagged="..." --args |
Pass no arguments, instead of passing run flags to the script | run -r --tagged="deploy" --parallel --args |
| Command | Description | Example |
|---|---|---|
run manage script-add <path> <name> [docs] [--force] |
Add a script (supports .py, .sh, .bat, .lua, .task.lua, .nd.task.lua) |
run manage script-add ./deploy.py deploy "Deploy to production" |
run manage script-remove <name> |
Remove a script from the config | run manage script-remove mypy |
run manage list |
Show the list of registered scripts | run manage list |
run manage tag <name> <tags...> |
Add/remove tags. Prefix a tag with ! to remove it |
run manage tag mypy deploy !prod dev |
run manage install <url> [name] [description] [--force] [--args="..."] |
Install a script from an external source, or run a tal installation script | run manage install github.com/user/repo@main/deploy.py |
run manage templ-add <ext> [file] [--source="..."] [--force] |
Add a wrapper template for an extension | run manage templ-add ".go" templ.txt |
run manage templ-remove <ext> |
Remove a template | run manage templ-remove ".go" |
Aliases: scradd = script-add, screm = script-remove, tladd = templ-add, tlrem = templ-remove.
For details: run manage help.
| Command | Description | Example |
|---|---|---|
run sys version |
Show the versions of run and tal | run sys version (alias: run sys -v) |
run sys localmode |
Show the current mode and the config path | run sys localmode |
run sys localmode true |
Enable local mode | run sys localmode true |
run sys localmode false |
Disable local mode | run sys localmode false |
Alias: -lm = localmode.
All tal commands are available through run tal .... See the tal README.
run tal init
run tal update
run tal list main.task.lua
run tal run main.task.lua buildBuilt-in tap flags
--verbose- verbose output.--debug- debug output.-h,-help,help- help.--no_color- disable colored output for the session.
By default run works globally (config in ~/run/).
Enable local mode - and run will use .run/ in the current folder:
run sys localmode true # enable
run sys localmode false # disable
run sys localmode # show stateThis is convenient for projects: scripts are stored in the repository and do not interfere with the global config.
The flags --lm, --localmode, --gm, --globalmode - placed immediately after run - switch the mode
only for the current invocation, and afterwards the mode set with run sys localmode is restored.
run --localmode manage list # show local scripts
run --globalmode -r deploy # run a global script
run --lm manage install github.com/pt-main/run-scripts@main/sysfetch.luaImportant: the --localmode / --globalmode flag must come immediately after run.
run automatically generates Lua wrappers that call the original scripts with the passed arguments.
Built in:
| Extension | Language | Note |
|---|---|---|
.py |
Python | Looks for python3, then python |
.sh |
Bash | Executes via bash |
.bat |
Batch | Executes via cmd /c |
.lua |
Lua | Executes directly (without a wrapper) |
.task.lua |
Task Lua (Tal) | Runs the file as a tal task file (with dependencies) |
.nd.task.lua |
Task Lua (Tal) | Same, but with dependency checking disabled (nd - no deps) |
Any other extension is available through a custom wrapper template.
For extensions that are not built in, you can add your own wrapper template. A template is a
Go text/template file, and its output becomes the Lua script
saved to scripts/<name>.lua.
The template body is not stored in the config: it lives in the templates/ directory, and
config.tycl keeps only a file reference:
templates: objects = [
{
ext: string = ".rb",
file: string = "rb.templ", // file inside templates/
}
],
| Variable | Value |
|---|---|
{{.ext}} |
File extension, e.g. .rb, .go |
{{.name}} |
Internal name of the file inside base/ (used in script_path(...)) |
The wrapper gets its generated name via script_path("{{.name}}") - this is how it finds the original
script in base/.
When run manage script-add runs, the template is chosen like this:
- If the file name ends with
.nd.task.luaor.task.lua- the built-in tal template is used. - Otherwise a custom template is looked up, whose
extis a suffix of the file name (e.g..rbforscript.rb). - If nothing matched - the template with an empty
ext(fallback) is taken. - If there is no fallback either - the built-in templates for
.py,.sh,.bat,.lua. - If nothing fits - the error
Unsupportable file extension.
# from a file (the content is copied to templates/<ext>.templ)
run manage templ-add ".rb" ruby-template.lua
# from a string
run manage templ-add ".rb" --source='...'
# replace an existing one
run manage templ-add ".rb" ruby-template.lua --force
# remove (the file in templates/ is removed too)
run manage templ-remove ".rb" # or: run manage tlrem ".rb"
filemay also be an absolute or relative path - if it is absolute, it is read as is, otherwise it is looked up in thetemplates/directory.
File ruby-template.lua:
-- === CONFIGURATION ===
local script_file = script_path("{{.name}}")
local args = get_args()
-- =====================
local function escape(arg)
if arg:match("[ \t\"']") then
return '"' .. arg:gsub('"', '\\"') .. '"'
end
return arg
end
local cmd = "ruby " .. escape(script_file)
for _, a in ipairs(args) do
cmd = cmd .. " " .. escape(a)
end
local result = os.execute(cmd)
os.exit(result or 0)Add it and use it:
run manage templ-add ".rb" ruby-template.lua
run manage script-add ./my_tool.rb mytool "My Ruby tool"
run -r mytool arg1 arg2- A template is a Go template, not Lua.
{{.ext}}and{{.name}}are substituted before the file is written toscripts/. - The template body is the Lua code of the future wrapper. It must terminate correctly (
os.exit(...)). - One
ext= one template. Use--forceto replace an existing one. - Template bodies live in
templates/, and thetemplatesfield ofconfig.tyclonly keeps{ext, file}- the config stays readable and templates can be edited and highlighted by ordinary editors. - Old configs, where the template body was kept inline in the
templatefield, are migrated automatically: the body is moved totemplates/<ext>.templand onlyfileis left in the config. The migration happens once, when the config is read.
~/run/
├── config.tycl # Config in TYCL (strict contract)
├── scripts/ # Lua wrappers for launching
│ └── myscript.lua
├── templates/ # Bodies of custom wrapper templates
│ └── rb.templ
└── base/ # Original scripts
└── myscript.py
Script configuration is built on Tycl - a typed language with the concept of contracts (fixed config formats).
Config contract -
strict {
scripts: objects = strict {
name: string, // Script name (command)
script: string, // Name of the wrapper file (matches the Lua script name inside run/scripts, without extension)
description: string, // Description
tags: strings, // Tags
ext: string, // Extension (.py, .sh, .bat, .lua, .task.lua)
},
templates: objects = strict {
ext: string, // File extension
file: string, // Template body file inside the templates/ dir
},
}
The config is filled in automatically by the run CLI; after the first launch it looks like this -
{
templates: objects = [],
scripts: objects = [
{
name: string = "test",
script: string = "test",
description: string = "[?BBK]Simple script for functions test[?RT]",
ext: string = "",
tags: strings = ["__test"],
}
],
}
Each wrapper is a Lua script that provides:
script_path(name)- path to the original script.get_arg(idx)- get an argument by index.get_args()- table of all arguments.run_script(name, ...)- run another script from the wrapper.run_script_parallel(name, ...)- runs the specified script asynchronously in a background thread. Does not block execution of the current script. All arguments after the name are passed to the called script.wait()- waits for all background scripts started viarun_script_parallelto finish. It is recommended to call it after starting parallel tasks to wait for their completion before the main script exits.run_cli(args)- run the run cli with the passed arguments (as a string) in the current session.
Example:
run_script_parallel("build", "--release")
run_script_parallel("test")
wait() -- wait for the build and tests to finishrun manage script-add ~/projects/tools/deploy.py deploy "Deploy to production"
run manage list
# ╭─────── Scripts
# ⎬─ deploy (.py):
# │ Deploy to production
# ╰───────Alias:
run manage scradd ~/projects/tools/deploy.py deploy "Deploy to production"run -r deploy --env=prod
# or
run deploy --env=prod # when the script name does not conflict with run commandsrun manage tag deploy prod utils
run -r --tagged="prod" # will run all scripts with the prod tagRemoving a tag:
run manage tag deploy !utils# a plain script file
run manage install github.com/user/repo@main/deploy.py deploy "Prod deploy"
# a tal installation script
run manage install github.com/user/repo@main/run.task.lua --args="--version 1.2.3"cd ~/myproject
run sys localmode true
run manage script-add script.py build
# now the script will be saved in .run/or for a single invocation:
run --localmode manage script-add script.py buildrun sys version
# or
run sys -vApache 2.0 - details in LICENSE.
By Pt, 2026 - written using lc, tap, pack, tycl.