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