ZSH map and filter functions using “anonymous functions”

UPDATE: Arash Rouhani picked this up and took it quite a bit further here.

The other day I was thinking that it would be handy to have a map function (in the functional programming sense) for zsh, and I couldn’t see anything in zsh itself that looked like what I wanted. So a quick google turned up this page by Yann Esposito, which gives an implementation of not only map but also filter and fold. Very cool!

The only problem, as the author points out, is that it is inconvenient to have to actually define a separate named function in order to use the facilities. So I groveled around in the zsh docs and found the (e) qualifier for parameter expansion, which causes an evaluation. That led me to write new versions of map and filter and related commands that work with anonymous “functions” — really just bits of code that get evaluated with $1 set to something — like so:

### Map each of a list of integer Xs to X+1:

$ mapa '$1+1' {1..4}
2
3
4
5

### Map each FOO.scala to FOO.class:

$ map '$1:r.class' test{1,2}.scala
test1.class
test2.class

### Get the subset which are ordinary files (bin is a dir):

$ filterf 'test -f' bin test{1,2}.scala
test1.scala
test2.scala

### Get the even numbers between 1 and 5:

$ filtera '$1%2 == 0' {1..5}
2
4

### Map each filename to 0 if it is an ordinary file:

$ each '[ -f $1 ]; echo $?' /bin test{1,2}.scala
1
0
0

### Given a directory tree containing some Foo.task files,
### an isStartable function that returns success if a task is startable now,
### and a startTask function that starts it, start all startable tasks.

$ eachf startTask $( filterf isStartable **/*.task )

Here are the functions:

###### map{,a}

### map Word Arg ...
### For each Arg, evaluate and print Word with Arg as $1.
### Returns last nonzero result, or 0.

function map() {
  typeset f="$1"; shift
  typeset x
  typeset result=0
  for x; map_ "$x" "$f" || result=$?
  return $result
}
function map_() {
  print -- "${(e)2}"
}

### mapa ArithExpr Arg ...   # is shorthand for
### map '$[ ArithExpr ]' Arg ...

function mapa() {
  typeset f="\$[ $1 ]"; shift
  map "$f" "$@"
}

###### each{,f}

### each Command Arg ...
### For each Arg, execute Command with Arg as $1.
### Returns last nonzero result, or 0.

function each() {
  typeset f="$1"; shift
  typeset x
  typeset result=0
  for x; each_ "$x" "$f" || result=$?
  return $result
}
function each_() {
  eval "$2"
}

### eachf Command Arg ...   # is shorthand for
### each 'Command $1' Arg ...

function eachf() {
  typeset f="$1 \"\$1\""; shift
  each "$f" "$@"
}

###### filter{,f,a}

### filter Command Arg ...
### For each Arg, print Arg if Command is successful with Arg as $1.

function filter() {
  typeset f="$1"; shift
  typeset x
  for x; filter_ "$x" "$f"
  return 0
}
function filter_() {
  eval "$2" && print -- "$1"
}

### filterf Command Arg ...   # is shorthand for
### filter 'Command "$1"' Arg ...

function filterf() {
  typeset f="$1 \"\$1\""; shift
  filter "$f" "$@"
}

### filtera ArithRelation Arg ...  # is shorthand for
### filter '(( ArithRelation ))' Arg ...

function filtera() {
  typeset f="(( $1 ))"; shift
  filter "$f" "$@"
}

Writing this kind of code is tricky for me; it’s easy to get the quoting wrong. For example,

$ each 'echo "$1"' \*

should just print an asterisk, but at first I was getting a list of files.

Anyway, it was fun. I might add fold and friends later, when I have more time.

Oh, one last thing. I use this function for testing zsh code:

TEST() {
  echo TEST: "${(qq)@}"
  "$@"
}

That way I can write a suite of many tests like this one

TEST filtera '$1%2 == 0' {1..5}

and get output like this, showing me what is being tested, followed by the results:

TEST: 'filtera' '$1%2 == 0' '1' '2' '3' '4' '5'
2
4

Without the (qq) you wouldn’t be able to tell where one argument ends and the next begins:

TEST: filtera $1%2 == 0 1 2 3 4 5
2
4

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.