Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/abo-abo/cook

Makefile -> Cookbook.py
https://github.com/abo-abo/cook

emacs makefile python

Last synced: about 2 months ago
JSON representation

Makefile -> Cookbook.py

Awesome Lists containing this project

README

        

* 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=):
#+begin_src sh
pip3 install --upgrade pycook
# or system-wide: sudo pip3 install --upgrade pycook
#+end_src

To hook up bash completion for =cook=, add to your file:~/.bashrc:
#+begin_src sh
. ~/.local/cook/bash-completion.sh
# or system-wide: . /usr/local/cook/bash-completion.sh
#+end_src

* Running recipes
You can run the recipes with =cook =.

Minimal =Cookbook.py= example:
#+begin_src python
#* 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"]
#+end_src

Runing e.g.:
#+begin_src sh
cook files_in_dir
#+end_src

will call:
#+begin_src python
from Cookbook import *
bash(files_in_dir(42))
#+end_src

* 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:
#+begin_src sh
cook :net ip
#+end_src

Here's a recipe with args, and the equivalent =sed= command:
#+begin_src sh
echo foo:bar:baz | cook :str split :
echo foo:bar:baz | sed -e 's/:/\n/g'
#+end_src

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=:

#+begin_src python
def bar(recipe):
print("Hello, World!")
#+end_src

You can run it like this:
#+begin_src sh
cook :foo bar
#+end_src

* 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:

#+begin_src python
config = {
"*": {
"tee": {
"location": "/tmp/cook"
}
}
}
#+end_src

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:
#+begin_src elisp
(global-set-key [f6] 'cook)
#+end_src

** Elisp completion in =shell=
Thanks to [[https://github.com/szermatt/emacs-bash-completion][bash-completion.el]], the completion in =M-x shell= works nicely as well.

I especially like completion for:
#+begin_src sh
cook :apt install python-
#+end_src

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:
#+begin_src python
def file_to_package(recipe, fname):
return ["dpkg -S " + fname]
#+end_src

You can call them like this:
#+begin_src sh
cook :dpkg file_to_package /usr/bin/python
#+end_src

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:
#+begin_src python
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])
#+end_src

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
#+begin_src python
def build(recipe, mode=["debug","release"]):
...
#+end_src
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:
#+begin_src python
def psql(recipe, config={"pty": True}):
return "psql $DATABASE_URL"
#+end_src
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:
#+begin_src python
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/"]
#+end_src

** Generate recipes using function-writing functions
#+begin_src python
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")
#+end_src

** Remove a recipe temporarily
Obviously, you can comment it out. But a faster approach is to delete its variable.
#+begin_src python
def dont_need_it_this_month(recipe):
# ...
return

del dont_need_it_this_month
#+end_src

** Alternatives

- [[https://www.fabfile.org/][Fabric]]/[[https://www.pyinvoke.org/][Invoke]]