Pathaction | A universal Makefile for any file in the filesystem: Rule-driven commands for any file or directory
The pathaction tool is a flexible command-line utility for running commands on files and directories. Pass a file path as an argument, and the tool handles the rest, whether you are working with code, media, or configurations.
Think of pathaction like a Makefile for any file or directory in the filesystem. It uses a .pathaction.yaml file to determine which command to run, and you can use Jinja2 templating to make those commands dynamic. You can also use tags to define multiple actions for the exact same file type. For example, you can set up one tag to run a script, another to debug it, and a third to run a linter.
This tool is built for software engineers who manage multiple projects across diverse environments. It eliminates the cognitive load of switching between different build tools, environment configurations, and deployment methods. Run a single unified command on any file and trust that it gets handled correctly.
If this tool helps your workflow, please show your support by ⭐ starring pathaction on GitHub and sharing it on your website, blog, Mastodon, Reddit, X, LinkedIn, or other social media platforms to help more Git users discover its benefits.
You can execute a file with the following commands:
pathaction -t main file.pyOr:
pathaction -t edit another-file.jpgThe -t option specifies the tag, allowing you to apply a tagged rule.
Here is an example of what a .pathaction.yaml rule-set file looks like:
---
actions:
- path_match: "*.py"
tags: main
command:
- "python"
- "{{ file }}"
- path_match: "*.jpg"
tags:
- edit
- show
command: "gimp {{ file|quote }}"There are many ways to match paths, including using regular expressions and MIME types. See below for more details.
- Emacs: pathaction.el
- Vim: vim-pathaction
- Other editors: Contributions are welcome.
Here is how to install pathaction using pip:
pip install --user pathactionThe pip command above will install the pathaction executable in the directory ~/.local/bin/.
(Python requirements: jinja2, schema, PyYAML.)
The pathaction CLI offers optional dependencies that extend its functionality. These extras can be installed according to environment requirements.
-
Colored Terminal Output (
colors): Installscoloramato provide consistent cross-platform ANSI color support. This enhances the readability of standard output and error messages.pip install "pathaction[colors]" -
Custom Process Title (
proctitle): Installssetproctitleto rename the running process frompythontopathaction. This simplifies identification in system monitoring tools such astop,htop, andps.pip install "pathaction[proctitle]"
To install both extras at once, use a comma-separated list:
pip install "pathaction[colors,proctitle]"By default, pathaction does not read rule-set files from arbitrary directories for security reasons. The allowed directories must be explicitly permitted. To allow pathaction to load rules from your projects folder and its subdirectories, run:
pathaction --allow-dir ~/projectsNavigate to your project root and create a .pathaction.yaml file (e.g., ~/projects/my-app/.pathaction.yaml). Here is a basic example to run Python files:
---
actions:
- path_match: "*.py"
tags: main
command:
- "python"
- "{{ file }}"Now, instead of manually invoking the Python interpreter, you can pass the file directly to pathaction. It will read the rule, match the .py extension, and execute the command.
pathaction ~/projects/my-app/dir1/dir2/file.pyBecause the default tag is main, pathaction automatically targets the block we just defined.
You can also specify the tag:
pathaction -t main ~/projects/my-app/dir1/dir2/file.pyThe real value of pathaction comes from defining complex, multi-tag workflows across different file types. Here are detailed examples of how to configure .pathaction.yaml for various scenarios.
Instead of memorizing different tools and their command-line arguments, you can define them as actions.
---
actions:
# Execute the Python script
- path_match: "*.py"
tags: main
command:
- "python"
- "{{ file }}"
# Run tests
- path_match: "*.py"
tags: test
command: "pytest -v {{ file|quote }}"
# Type checking
- path_match: "*.py"
tags: typecheck
command: "mypy {{ file|quote }}"
# Execute Bash shell scripts
- path_match: "*.sh"
tags:
- main
command: "bash {{ file|quote }}"You can invoke these specific actions by passing the -t (tag) flag:
pathaction -t typecheck src/utils.py
pathaction -t test src/test_utils.py
You can set up rules to apply Ansible playbooks directly.
---
actions:
- path_match: "*playbook.yml"
tags: main
command: "ansible-playbook -i inventory.ini {{ file|quote }}"
You can use pathaction to compile files on the fly and put the output binary in the correct directory.
---
actions:
# Compile C++ source code
- path_match: "*.cpp"
tags: build
cwd: "{{ file|dirname }}"
command: "g++ -Wall -O2 {{ file|quote }} -o {{ file|basename|replace('.cpp', '') }}"
# Run the compiled binary
- path_match: "*.cpp"
tags: run
cwd: "{{ file|dirname }}"
command: "./{{ file|basename|replace('.cpp', '') }}"
The pathaction utility accepts several arguments to control execution behavior:
-t,--tag: Execute the action associated with this tag (default ismain).-b,--confirm-before: Prompt for confirmation before executing the defined action.-a,--confirm-after: Ask the user if they want to execute the action again after it finishes (respects theconfirm_after_timeoutconfiguration).-l,--list: List all the.pathaction.yamlconfiguration files that apply to the given file path. Useful for debugging rule resolution.-d,--allow-dir: Permanently allowpathactionto execute rules from the provided directory and its subdirectories.--disallow-dir: Revoke access and remove a specific directory from your allowed list.--list-allowed-dirs: Print a list of all permanently allowed directories.
The rule-sets cascade hierarchically. When you execute a file, pathaction looks for .pathaction.yaml in the file's current directory, and then walks up the filesystem tree (parent directories) to find and merge all other .pathaction.yaml files. This loading behavior is similar to that of a .gitignore file. In case of conflicting rules or configurations, priority is given to the rule set that is located in the directory closest to the specified file.
Each rule defined in the rule-set file must include at least the matching rule and the command.
You can target files using various matching strategies. Every match method also has a corresponding _exclude variant (e.g., path_match_exclude) to explicitly ignore files that would otherwise match.
path_match: Standard glob pattern matching (e.g.,*.py).path_match_case: Case-sensitive glob pattern matching.path_regex: Regular expression path matching (case-insensitive by default).path_regex_case: Case-sensitive regular expression matching.mimetype: Strict MIME type matching (e.g.,text/x-python).mimetype_match: Glob pattern matching for MIME types (e.g.,text/*).mimetype_regex: Regular expression matching for MIME types.
An action block can include the following optional attributes:
tags: A string or list of strings indicating the tag names.comment: An informative string describing what the action does.timeout: An integer specifying the maximum execution time in seconds.cwd: The current working directory for the command.stdout: Redirect the standard output of the command to the specified file path.stderr: Redirect the standard error of the command to the specified file path. (If bothstdoutandstderrpoint to the exact same file path, they are combined).shell: Boolean indicating if the command should be executed within a shell environment.command/list_commands: The command string/array to execute. These two are mutually exclusive.
You can define an options block at the root of your .pathaction.yaml to specify execution defaults:
shell_path: The absolute path to the shell executable used whenshell: true(defaults to the user's login shell).verbose: Enable verbose logging.debug: Enable debug mode.timeout: A global timeout constraint in seconds.confirm_after_timeout: The timeout in seconds when waiting for user input during a--confirm-afterprompt.last: If set totrue,pathactionstops loading configurations from higher parent directories.
| Variable | Description |
|---|---|
{{ file }} |
Replaced with the full absolute path to the targeted file. |
{{ cwd }} |
Refers to the current working directory of the matched action. |
{{ env }} |
Represents the operating system environment variables (dictionary). |
{{ pathsep }} |
Denotes the path separator (e.g., / on Linux, \ on Windows). |
quote: Escapes a string for use as a shell argument by wrapping it in single quotes and escaping internal single quotes. This prevents shell injection vulnerabilities. Example:"/home/user/my file.txt" | quoteevaluates to'/home/user/my file.txt'.basename: Extracts the trailing filename or leaf component of a filesystem path. Example:"/home/user/src/main.py" | basenameevaluates to"main.py".dirname: Returns the parent directory portion of a filesystem path. Example:"/home/user/src/main.py" | dirnameevaluates to"/home/user/src".file_only_dirname: Returns the parent directory if the path is a file, or returns the path itself if it is already a directory.realpath: Resolves all symbolic links, relative segments (like..), and duplicate separators to return the canonical absolute path. Example:"/usr/bin/../local/bin/python" | realpathevaluates to"/usr/local/bin/python".abspath: Converts a relative path into an absolute path by prefixing it with the current working directory. Example:"src/main.py" | abspathevaluates to"/home/user/project/src/main.py".relpath: Computes the relative path between two directories.joinpath: Combines one or more path segments using the system filesystem separator. Example:"/var/log" | joinpath("nginx", "error.log")evaluates to"/var/log/nginx/error.log".joincmd: Converts an array of command-line tokens into a single properly escaped shell command string. Example:["grep", "-i", "error log"] | joincmdevaluates to'grep -i "error log"'.splitcmd: Parses a raw shell command string into an array of distinct arguments while honoring quotation rules and escape sequences. Example:"git commit -m 'initial release'" | splitcmdevaluates to["git", "commit", "-m", "initial release"].expanduser: Replaces a leading tilde notation (~or~user) with the absolute path of the corresponding user home directory. Example:"~/config/tmux.conf" | expanduserevaluates to"/home/user/config/tmux.conf".expandvars: Substitutes environment variables within a string matching$VARIABLEor${VARIABLE}with their current active system values. Example:"$HOME/.config" | expandvarsevaluates to"/home/user/.config".shebang: Inspects a file and extracts the first line directly if it begins with an executable script prefix (#!). Example:"/home/user/script.sh" | shebangevaluates to"#!/usr/bin/env bash".shebang_list: Extracts the shebang line from a file, discards the initial#!marker, and parses the remaining contents into a clean token array. Example:"/home/user/script.sh" | shebang_listevaluates to["/usr/bin/env", "bash"].shebang_quote: Extracts the shebang line from a file, strips the#!marker, and returns the runtime interpreter directive as a safely balanced, shell-quoted string. Example:"/home/user/script.sh" | shebang_quoteevaluates to"/usr/bin/env bash".which: Searches the system environment variablePATHto locate the absolute path of an executable binary. Raises an error if the binary cannot be found. Example:"emacs" | whichevaluates to"/usr/bin/emacs".startswith: Evaluates to true if the string starts with the given prefix.endswith: Evaluates to true if the string ends with the given suffix.
Does pathaction walk the filesystem from the current directory to the top in search of .pathaction.yaml ruleset files?
Pathaction walks from the directory containing the file passed to it and merges .pathaction.yaml rules from all allowed parent directories.
There is a security measure by default: loading rules is allowed only in directories that have been explicitly permitted using pathaction --allow-dir ~/dir/projects/, which enables access to ~/dir/projects/ and all its subdirectories. If the entire home directory is allowed with pathaction --allow-dir ~/, rules can be loaded from any directory within the home directory.
The make tool centers on targets and dependency tracking, making it good for compiling software based on file timestamps. In contrast, the pathaction tool acts as a universal file execution router. Passing a file path directly to Pathaction determines the correct command to run based on defined file extensions or patterns.
While make relies on project-specific files with strict syntax, Pathaction uses YAML files that cascade hierarchically across your filesystem. Much like how Git handles ignore files, Pathaction loads and merges all .pathaction.yaml rule-set files found in parent directories. This allows you to define rules in your home directory that can be overridden by specific settings within individual project folders.
For example, a Python script in ~/project_a can be routed to a local virtual environment, while a Python script in ~/project_a/project_b can trigger a Docker execution simply by defining different .pathaction.yaml files in those directories. Pathaction loads and merges all .pathaction.yaml ruleset files found in parent directories. This means that any rule in ~/project_a/project_b/.pathaction.yaml that does not match a file falls back to the rules defined in ~/project_a/.pathaction.yaml, similar to how Git handles .gitignore files.
It is very different from find | xargs. The pathaction tool functions like a customizable, developer-focused xdg-open. It acts as the intelligent router that receives each file path and automatically determines the correct command to execute based on your defined rules. Just as xdg-open relies on rigid system MIME types to launch GUI applications, Pathaction uses your hierarchical .pathaction.yaml configurations and Jinja2 templating to dynamically run commands.
Shebangs are fine for basic execution, but they have limitations that Pathaction was built to address.
A shebang only defines how to execute a script. It cannot tell your system how to lint, format, debug, or test files. With pathaction, you can use tags. Passing pathaction -t main file.py executes it, while passing pathaction -t test file.py can run it through pytest.
File associations such as xdg-open apply globally. Pathaction uses cascading YAML files similar to how Git handles .gitignore files. A Python script in ~/project_a can be routed to a local virtual environment, while a Python script in ~/project_a/project_b can trigger a Docker execution simply by defining different .pathaction.yaml files in those directories. Pathaction loads and merges all .pathaction.yaml ruleset files found in parent directories. This means that any rule in ~/project_a/project_b/.pathaction.yaml that does not match a file falls back to the rules defined in ~/project_a/.pathaction.yaml.
In addition to that, Pathaction uses Jinja2 templating, allowing you to dynamically build complex shell commands based on the file name, its parent directory, or environment variables.
The author's .pathaction.yaml rules function as a universal bridge across distinct software environments.
- For Python, Bash, C, C++, and related languages, rules are defined to install dependencies, build projects, execute binaries, run test suites, and launch debuggers.
- For Ansible, rules automatically upload playbooks to remote servers, execute them, and validate their results.
- For Emacs, rules integrate file-based actions directly with editor workflows, enabling evaluation, compilation, or linting based on context.
- For Vim, rules provide similar editor integration, allowing files to trigger build, run, or formatting actions without manual command construction.
Pathaction operates as an IDE-like action layer for the filesystem. Instead of embedding logic inside each editor or build system, actions are described declaratively and applied uniformly across tools.
The primary advantage is cognitive simplicity. There is no need to memorize complex command-line flags or tool-specific invocation patterns. A file is passed to Pathaction with a semantic tag such as main, install, or debug, and the corresponding rule determines how the operation is executed.
Copyright (c) 2021-2026 James Cherti
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
Plugins for editors:
- pathaction.el (Emacs package): Executing the
pathactioncommand-line tool directly from Emacs. - vim-pathaction (Vim plugin): Executing the
pathactioncommand-line tool directly from Vim.