Makefile -> Cookbook.py.


Keywords
makefile, automation, emacs, python
License
GPL-3.0+
Install
pip install pycook==0.11.0

Documentation

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 recipe and returns a vector of strings - a sequence of commands to run.

Installing

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

Running 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_dir

will 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 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 bat
  • cook 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.

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 bar

Automatic 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/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.

Completion for variants in recipe args

def build(recipe, mode=["debug","release"]):
    ...

Invoking this recipe with el:cook, we get completion for mode using el:ivy-read. TODO: implement bash completion for this.

Custom recipe config

Some commands require a PTY in order to be run correctly. Here’s how the recipe can enable it:

def psql(recipe, config={"pty": True}):
    return "psql $DATABASE_URL"

For these commands, a history file is setup in ~/.cook.d/history. This allows, for example, to automatically have a separate history when connecting to different databases.

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, publish
from pycook.recipes.emacs import byte_compile as emacs_byte_compile

def lint(recipe):
    return ["pylint pycook/"]

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

Alternatives