Introduction -*- mode: org -*-
The traditional Makefile is replaced by Cookbook.py. Use cook in place
of make.
This results in a lot of flexibility, because the new “Makefile” is scripted in a full-featured language - Python.
- Recipe
- the equivalent of a Makefile’s rule
- A Python function that takes a single argument called
recipeand returns a vector of strings - a sequence of commands to run.
- A Python function that takes a single argument called
Installing
Install for user (ensure file:~/.local/bin is in your PATH):
pip3 install --upgrade pycook
# or system-wide: sudo pip3 install --upgrade pycookTo hook up bash completion for cook, add to your file:~/.bashrc:
. ~/.local/cook/bash-completion.sh
# or system-wide: . /usr/local/cook/bash-completion.shRunning recipes
You can run the recipes with cook <recipe>.
Minimal Cookbook.py example:
#* Recipes
def files_in_dir(recipe):
return ["ls"]
def files_in_parent_dir(recipe):
res = ["cd .."]
res += ["find ."]
return res
def last_commit(recipe):
return ["git rev-parse HEAD"]Runing e.g.:
cook files_in_dirwill call:
from Cookbook import *
bash(files_in_dir(42))Running global recipes
You can also run “global” recipes, which are installed together with
pycook.
As example of a recipe without args, this will show you your current IP address:
cook :net ipHere’s a recipe with args, and the equivalent sed command:
echo foo:bar:baz | cook :str split :
echo foo:bar:baz | sed -e 's/:/\n/g'Both commands have the same length, but:
sedis more generic, harder to remember, harder to get right off the batcookis less generic, but easy to remember and discoverable
All global recipes start with cook :. Pressing TAB after that should
show all available recipe modules, like e.g. str or net or pip etc.
After selecting a module, pressing TAB should show all available
recipes within the module.
Finally, you enter the arguments to the recipe if it has any.
Running user recipes
Users can add to the global recipes list by placing Python files in
~/.cook.d/.
Example, ~/.cook.d/foo.py:
def bar(recipe):
print("Hello, World!")You can run it like this:
cook :foo barAutomatic logging
cook can automatically log your stdout to a file with a
timestamped name in a location you specify.
To make use of this, create a file ~/.cook.d/__config__.py with the following contents:
config = {
"*": {
"tee": {
"location": "/tmp/cook"
}
}
}Here, the inital =”*”= is used to select all books. It’s possible to customize each book separately by using the file name as a key.
Elisp completion
It’s actually much more convenient to use cook from Emacs.
The main advantage is that Emacs will find the appropriate Cookbook.py from anywhere in the project.
The secondary advantages are:
- better completion for recipe selection
- the selected recipe is run in
compilation-mode, which connects any errors or warings to locations in a project. - the selected recipe is run in a buffer named after the recipe
- works with TRAMP, so the recipes from remote cookbooks will be run remotely.
M-x cook will:
- go recursively up from the current directory until a cookbook is found
- parse the cookbook for recipes
- offer the list of recipes
- run the seleted recipe in
compilation-mode
I’m using this binding:
(global-set-key [f6] 'cook)Elisp completion in shell
Thanks to bash-completion.el, the completion in M-x shell works nicely as well.
I especially like completion for:
cook :apt install python-Thanks to ivy-mode, I can easily select from 3805 packages in Ubuntu that with “python-“.
One extra plus of cook :apt install over apt-get install is that it will not ask for sudo
if it’s not required (i.e. when the package is already installed).
Custom recipe completion
For recipes can have extra arguments:
def file_to_package(recipe, fname):
return ["dpkg -S " + fname]You can call them like this:
cook :dpkg file_to_package /usr/bin/pythonWhile getting completion for the :dpkg and file_to_package parts is automatic, for the
third argument it’s not, since it could be anything. However, in this case, since the
argument is named fname, an automatic file name completion is provided.
Here’s how to implement a manual file name completion:
def file_to_package(recipe, fname):
if type(recipe) is int:
return ["dpkg -S " + fname]
elif recipe[0] == "complete":
return el.sc("compgen -o filenames -A file " + recipe[1])The use of compgen isn’t mandatory: all it does is return a string of the possible
completions separated by newlines.
Useful Python tricks for Cookbook.py
Don’t write recipes if you can import them
For example, here’s this repo’s Cookbook.py:
from pycook.recipes.pip import clean, sdist, reinstall
def publish(recipe):
return [
"rm -rf dist/",
"python3 setup.py sdist",
"twine upload dist/*"
]Generate recipes using function-writing functions
import shlex
def open_in_firefox(fname):
def result(recipe):
return ["firefox " + shlex.quote(fname)]
return result
open_README = open_in_firefox("README")
open_Cookbook = open_in_firefox("Cookbook.py")Remove a recipe temporarily
Obviously, you can comment it out. But a faster approach is to delete its variable.
def dont_need_it_this_month(recipe):
# ...
return
del dont_need_it_this_month