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
recipe
and returns a vector of strings - a sequence of commands to run.
- A Python function that takes a single argument called
Install for user (ensure file:~/.local/bin is in your PATH
):
pip3 install --upgrade pycook
# or system-wide: sudo pip3 install --upgrade pycook
To hook up bash completion for cook
, add to your file:~/.bashrc:
. ~/.local/cook/bash-completion.sh
# or system-wide: . /usr/local/cook/bash-completion.sh
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_dir
will call:
from Cookbook import *
bash(files_in_dir(42))
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 ip
Here’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:
sed
is more generic, harder to remember, harder to get right off the batcook
is 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.
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 bar
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.
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)
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).
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/python
While 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.
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/*"
]
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")
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