geophile / marcel

A modern shell
https://marceltheshell.org
GNU General Public License v3.0
338 stars 13 forks source link
database postgres python scripting shell

What's New

You can assign the value of a Python expression to a variable, e.g.

x = (5 + 6)

assigns 11 to the variable x. The RHS, (5 + 6) is shorthand for (lambda: 5 + 6). In other words, the function inside the parens is invoked, and the resulting value is assigned.

Sometimes you want to assign a function to a variable. Following the previous example, to assign to inc a function that adds 1 to its input, you would have to do something like this:

inc = (lambda: lambda x: x + 1)

Evaluating (lambda: lambda x: x + 1) yields the function lambda x: x + 1 which is assigned to inc. That is kind of clunky, and while obviously correct and consistent, it isn't what you normally think of first. Or at least I didn't, I tried inc = (lambda x: x + 1) and got an error message because (lambda x: x + 1) cannot be evaluated without binding a value to x.

In this release, the less clunky, less consistent syntax is permitted. If you write:

inc = (lambda x: x + 1)

then inc will be assigned lambda x: x + 1. I.e., it will be understood that you really meant inc = (lambda: lambda x: x + 1). This only happens when assigning expressions to variables, and the parenthesized function has more than zero arguments. In other contexts, (e.g. the function used with map or select), there is no similar tweaking of the expression.

Marcel

Marcel is a shell. The main idea is to rely on piping as the primary means of composition, as with any Unix or Linux shell. However, instead of passing strings from one command to the next, marcel passes Python values: builtin types such as lists, tuples, strings, and numbers; but also objects representing files and processes.

Linux has extremely powerful commands such as awk and find. Most people know how to do a few simple operations using these commands. But it is not easy to exploit the full power of these commands due to their reliance on extensive "sublanguages" which do:

By contrast, marcel has no sublanguages. You use marcel operators combined with Python code to filter data, process it, and control command output.

The commands and syntax supported by a shell constitute a language which can be used to create scripts. Of course, in creating a script, you rely on language features that you typically do not use interactively: control structures, data types, and abstraction mechanisms (e.g. functions), for example. Viewed as a programming language, shell scripting languages are notoriously bad. I didn't think it was wise to bring another one into the world. So marcel takes a different approach, using Python as a scripting language, (see below for more on scripting).

Pipelines

Marcel provides commands, called operators, which do the basic work of a shell. An operator takes a stream of data as input, and generates another stream as output. Operators can be combined by pipes, causing one operator's output to be the next operator's input. For example, this command uses the ls and map operators to list the names and sizes of files in the /home/jao directory:

ls /home/jao | map (lambda f: (f, f.size))

A pipeline is a sequence of operators connected by pipes. They can be used directly on the command line, as above. They also have various other uses in marcel. For example, a pipeline can be assigned to a variable, essentially defining a new operator. For example, here is a pipeline, assigned to the variable recent, which selects Files modified within the past day:

recent = (| select (file: now() - file.mtime < days(1)) |) 

This pipeline can be used in conjunction with any pipeline yielding files. E.g., to locate the recently changed files in ~/git/myproject:

ls ~/git/myproject | recent

Functions

As shown above, a number of operators, like map and select, take Python functions as command-line arguments. Functions can also be invoked to obtain the value of an environment variable. For example, to list the contents of your home directory, you could write:

ls /home/(USER)

This concatenates the string /home/ with the string resulting from the evaluation of the expression lambda: USER. USER is a marcel environment variable identifying the current user, (so this command is equivalent to ls ~).

If you simply want to evaluate a Python expression, you could use the map operator, e.g.

map (5 + 6)

which prints 11. Marcel permits the map operator to be inferred, so this also works:

(5 + 6)

In general, you can elide map from any pipeline.

Executables

In addition to using built-in operators, you can, of course, call any executable. Pipelines may contain a mixture of marcel operators and host executables. Piping between operators and executables is done via streams of strings.

For example, this command combines operators and executables. It scans /etc/passwd and lists the usernames of users whose shell is /bin/bash. cat, xargs, and echo are Linux executables. map and select are marcel operators. The output is condensed into one line through the use of xargs and echo.

cat /etc/passwd \
| map (line: line.split(':')) \
| select (*line: line[-1] == '/bin/bash') \
| map (user, *_: user) \
| xargs echo

Shell Features

Marcel provides:

Scripting

Marcel's syntax for constructing and running pipelines, and defining and using variables and functions, was designed for interactive usage. Instead of extending this syntax to a full-fledged scripting language, marcel provides a Python API, allowing Python to be used as the scripting language. While Python is sometimes considered to already be a scripting language, it isn't really. Executing shell commands from Python code is cumbersome. You've got to use os.system, or subprocess.Popen, and write some additional code to do the integration.

Marcel provides a Python module, marcel.api, which brings shell commands into Python in a much cleaner way. For example, to list file names and sizes in /home/jao:

from marcel.api import *

for file, size in ls('/home/jao') | map(lambda f: (f, f.size)):
    print(f'{file.name}: {size}') 

This code uses the ls and map functions, provided by marcel.api. These correspond to the marcel operators ls and map that you can use on the command line. Output from the ls is a stream of Files, which are piped to map, which maps files to (file, file size) tuples. ls ... | map ... defines a pipeline (just as on the command line). The Python class representing pipelines defines iter, so that the pipeline's output can be iterated over using the standard Python for loop.

Installation

To install marcel locally (i.e., available only to your username):

python3 -m pip install marcel

This command installs marcel for the current user. To install for the entire system, use sudo python3 -m pip install --prefix ... instead. (The value of the --prefix flag should be something like /usr/local.)

Marcel depends on dill and psutil. These packages will be installed automatically if needed, when marcel is installed via pip.