about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--NEWS7
-rw-r--r--doc/eltest.html172
-rw-r--r--doc/index.html3
-rw-r--r--doc/upgrade.html3
-rw-r--r--package/deps.mak3
-rw-r--r--package/info2
-rw-r--r--package/modes1
-rw-r--r--package/targets.mak1
-rw-r--r--src/execline/deps-exe/eltest1
-rw-r--r--src/execline/eltest.c505
10 files changed, 692 insertions, 6 deletions
diff --git a/NEWS b/NEWS
index 475ed87..559fc17 100644
--- a/NEWS
+++ b/NEWS
@@ -1,9 +1,10 @@
 Changelog for execline.
 
-In 2.9.0.2
+In 2.9.1.0
 ----------
 
  - Adaptation to skalibs-2.9.0.2.
+ - New program: eltest.
 
 
 In 2.9.0.1
@@ -16,8 +17,8 @@ In 2.9.0.0
 ----------
 
  - Bugfixes.
- - New -a/-o options to wait (-o waits for one process only)
- - wait now exits 99 on timeout
+ - New -a/-o options to wait (-o waits for one process only).
+ - wait now exits 99 on timeout.
 
 
 In 2.8.3.0
diff --git a/doc/eltest.html b/doc/eltest.html
new file mode 100644
index 0000000..068cd4c
--- /dev/null
+++ b/doc/eltest.html
@@ -0,0 +1,172 @@
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta http-equiv="Content-Language" content="en" />
+    <title>execline: the eltest program</title>
+    <meta name="Description" content="execline: the eltest program" />
+    <meta name="Keywords" content="execline command eltest test" />
+    <!-- <link rel="stylesheet" type="text/css" href="//skarnet.org/default.css" /> -->
+  </head>
+<body>
+
+<p>
+<a href="index.html">execline</a><br />
+<a href="//skarnet.org/software/">Software</a><br />
+<a href="//skarnet.org/">skarnet.org</a>
+</p>
+
+<h1> The <tt>eltest</tt> program </h1>
+
+<p>
+ eltest evaluates an expression and indicates the result via its
+exit status.
+</p>
+
+<h2> Interface </h2>
+
+<pre>
+     eltest <em>expression...</em>
+</pre>
+
+<p>
+ <tt>eltest</tt> acts as the generic POSIX
+<a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html">test</a> utility,
+but it diverges from the specification on how it parses ambiguous arguments, see below.
+</p>
+
+<p>
+ <tt>eltest</tt> supports all the standard
+<a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html">test</a>
+operands, plus all the extensions from
+<a href="https://man7.org/linux/man-pages/man1/test.1.html">GNU test</a>, plus a few
+extensions from the <tt>test</tt> builtin from
+<a href="https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Bash-Conditional-Expressions">bash</a>.
+The extensions to POSIX <tt>test</tt> are listed below.
+</p>
+
+<p>
+ <tt>eltest</tt> accepts an arbitrary number of arguments and, if the expression is
+valid, always returns the result of the expression no matter how complex it
+is.
+</p>
+
+<h2> Exit codes </h2>
+
+<ul>
+ <li> 0: the test is true </li>
+ <li> 1: the test is false </li>
+ <li> 100: wrong usage </li>
+ <li> 101: internal error (should never happen, warrants a bug-report) </li>
+ <li> 111: system call failure </li>
+</ul>
+
+<h2> Posixness </h2>
+
+<p>
+ <tt>eltest</tt> <strong>is not</strong> suitable as a Single Unix
+<a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html">test</a>
+program, due to the way it disambiguates between arguments and operators, see below.
+However, if you never use arguments that start with a backslash, or that have the
+same name as an existing operator, then
+<tt>eltest</tt> exhibits the same behaviour as <tt>test</tt>.
+</p>
+
+<h2> Extensions to POSIX </h2>
+
+<ul>
+ <li> <tt><em>expr1</em>&nbsp;-a&nbsp;<em>expr2</em></tt>&nbsp;:
+tests whether <em>expr1</em> <strong>and</strong> <em>expr2</em> are true.
+If <em>expr1</em> is false, then <em>expr2</em> is not evaluated. </li>
+ <li> <tt><em>expr1</em>&nbsp;-o&nbsp;<em>expr2</em></tt>&nbsp;:
+tests whether <em>expr1</em> <strong>or</strong> <em>expr2</em> is true.
+If <em>expr1</em> is true, then <em>expr2</em> is not evaluated. </li>
+ <li> <tt>-k&nbsp;<em>file</em></tt>&nbsp;: tests whether <em>file</em>
+has the sticky bit. </li>
+ <li> <tt>-O&nbsp;<em>file</em></tt>&nbsp;: tests whether <em>file</em>
+is owned by the effective uid of the current process. </li>
+ <li> <tt>-U&nbsp;<em>file</em></tt>&nbsp;: same. </li>
+ <li> <tt>-G&nbsp;<em>file</em></tt>&nbsp;: tests whether <em>file</em>'s gid
+is the effective gid of the current process. </li>
+ <li> <tt>-N&nbsp;<em>file</em></tt>&nbsp;: tests whether <em>file</em> exists
+and has been modified since it was last read. </li>
+ <li> <tt><em>file1</em>&nbsp;-nt&nbsp;<em>file2</em></tt>&nbsp;:
+tests whether <em>file1</em> has a (strictly) newer modification date than <em>file2</em>. </li>
+ <li> <tt><em>file1</em>&nbsp;-ot&nbsp;<em>file2</em></tt>&nbsp;:
+tests whether <em>file1</em> has a (strictly) older modification date than <em>file2</em>. </li>
+ <li> <tt><em>file1</em>&nbsp;-ef&nbsp;<em>file2</em></tt>&nbsp;:
+tests whether <em>file1</em> and <em>file2</em> are physically the same
+file (same device and inode numbers). </li>
+ <li> <tt>-v&nbsp;<em>var</em></tt>&nbsp;: tests whether the
+<em>var</em> variable is defined in the current environment. </li>
+ <li> <tt><em>string</em>&nbsp;=~&nbsp;<em>pattern</em></tt>&nbsp;:
+tries to match <em>string</em> against extended regular expression
+<em>pattern</em>. True if any part of <em>string</em> matches <em>pattern</em>;
+in order to match whole strings, you must anchor <em>pattern</em> with
+<tt>^</tt> and <tt>$</tt> markers. </li>
+</ul>
+
+<h2> Argument disambiguation </h2>
+
+<p>
+ Unlike <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html">test</a>,
+which has different fixed syntax trees depending on the number of arguments it receives and
+has undefined behaviour when called with more than 5 arguments, <tt>eltest</tt> accepts any
+number of arguments and builds its syntax trees on the fly. This means that expressions such
+as <tt>-n = -n</tt> cannot be automatically disambiguated: <tt>eltest</tt> does not know that
+there are 3 arguments, so when it reads the first <tt>-n</tt> it assumes that it is an unary
+operator, then when it reads <tt>=</tt> it assumes it is the argument to <tt>-n</tt>, then
+when it reads the second <tt>-n</tt> it exits with a syntax error.
+</p>
+
+<p>
+ Doing otherwise would result in a combinatory explosion of possible syntax trees, making
+it easy for users to trigger unbounded RAM consumption, and turning a simple utility into
+a programming nightmare. This is why POSIX
+<a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html">test</a>
+is so restricted. But we don't want the same restrictions.
+</p>
+
+<p>
+ So, instead, <tt>eltest</tt> provides the user with a mechanism to make sure that
+operands are never mistaken for operators:
+</p>
+
+<ul>
+ <li> An word that looks like an operator will always be interpreted like an operator.
+So, expressions like <tt>-n = -n</tt> will result in a syntax error, because the
+first <tt>-n</tt> will never be understood as data for the <tt>=</tt> operator. </li>
+ <li> A word that starts with a <tt>\</tt> (backslash) will always be interpreted
+like data, never like an operator, and the backslash will be removed. This
+means: <tt>\-n = \-n</tt> is a valid expression testing the equality between
+the strings <tt>-n</tt> and </tt>-n</tt>.
+ <ul>
+  <li> Be aware that execline as well as the shell use one backlash for their own
+unquoting mechanism, so when using backslashes in an execline or shell script, they
+must be doubled. You would probably need to type something like <tt>\\-n = \\-n</tt>.
+ </ul> </li>
+ <li> So, if your script tests equality between <tt>$a</tt> and <tt>$b</tt>, and there's
+a possiblity that the contents of these variables look like <tt>eltest</tt> operators,
+the proper syntax would be: <tt>eltest \\${a} = \\${b}</tt>. </li>
+</ul>
+
+<p>
+ Note that these details are irrelevant to a huge majority of <tt>eltest</tt> use
+cases, because most of the time users only need a simple test
+such as <tt>eltest -r ${file}</tt> to check that <tt>$file</tt> is readable, and
+there's no possible ambiguity. So don't panic over this.
+</p>
+
+<h2> Notes </h2>
+
+<ul>
+ <li> <tt>eltest</tt> is a replacement for the ill-named, and now deprecated,
+<a href="https://skarnet.org/software/s6-portable-utils/s6-test.html">s6-test</a>
+program, part of the (just as ill-named)
+<a href="https://skarnet.org/software/s6-portable-utils/">s6-portable-utils</a>
+package. It is too valuable a utility to be part of a marginal package, and
+has nothing to do with <a href="https://skarnet.org/software/s6/">s6</a>. </li>
+</ul>
+
+</body>
+</html>
diff --git a/doc/index.html b/doc/index.html
index f9c985a..5d2f402 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -77,7 +77,7 @@ want nsswitch-like functionality:
 <h3> Download </h3>
 
 <ul>
- <li> The current released version of execline is <a href="execline-2.9.0.2.tar.gz">2.9.0.2</a>. </li>
+ <li> The current released version of execline is <a href="execline-2.9.1.0.tar.gz">2.9.1.0</a>. </li>
  <li> Alternatively, you can checkout a copy of the
 <a href="//git.skarnet.org/cgi-bin/cgit.cgi/execline/">execline
 git repository</a>:
@@ -194,6 +194,7 @@ the previous versions of execline and the current one. </li>
  (Miscellaneous)
 </p>
 <ul>
+<li><a href="eltest.html">The <tt>eltest</tt> program</a></li>
 <li><a href="homeof.html">The <tt>homeof</tt> program</a></li>
 </ul>
 
diff --git a/doc/upgrade.html b/doc/upgrade.html
index 26e119d..493c6e1 100644
--- a/doc/upgrade.html
+++ b/doc/upgrade.html
@@ -18,13 +18,14 @@
 
 <h1> What has changed in execline </h1>
 
-<h2> in 2.9.0.2 </h2>
+<h2> in 2.9.1.0 </h2>
 
 <ul>
  <li> <a href="//skarnet.org/software/skalibs/">skalibs</a>
 dependency bumped to 2.13.0.0. </li>
  <li> <a href="//skarnet.org/software/nsss/">nsss</a>
 optional dependency bumped to 0.2.0.2. </li>
+ <li> New program: <a href="eltest.html">eltest</a>. </li>
 </ul>
 
 <h2> in 2.9.0.1 </h2>
diff --git a/package/deps.mak b/package/deps.mak
index 17d654e..d9ab00a 100644
--- a/package/deps.mak
+++ b/package/deps.mak
@@ -10,6 +10,7 @@ src/execline/dollarat.o src/execline/dollarat.lo: src/execline/dollarat.c
 src/execline/elgetopt.o src/execline/elgetopt.lo: src/execline/elgetopt.c src/include/execline/execline.h
 src/execline/elgetpositionals.o src/execline/elgetpositionals.lo: src/execline/elgetpositionals.c src/include-local/exlsn.h
 src/execline/elglob.o src/execline/elglob.lo: src/execline/elglob.c src/include-local/exlsn.h
+src/execline/eltest.o src/execline/eltest.lo: src/execline/eltest.c
 src/execline/emptyenv.o src/execline/emptyenv.lo: src/execline/emptyenv.c src/include/execline/execline.h
 src/execline/envfile.o src/execline/envfile.lo: src/execline/envfile.c
 src/execline/exec.o src/execline/exec.lo: src/execline/exec.c
@@ -93,6 +94,8 @@ elgetpositionals: EXTRA_LIBS := -lskarnet
 elgetpositionals: src/execline/elgetpositionals.o ${LIBEXECLINE}
 elglob: EXTRA_LIBS := -lskarnet
 elglob: src/execline/elglob.o ${LIBEXECLINE}
+eltest: EXTRA_LIBS := -lskarnet
+eltest: src/execline/eltest.o
 emptyenv: EXTRA_LIBS := -lskarnet
 emptyenv: src/execline/emptyenv.o ${LIBEXECLINE}
 envfile: EXTRA_LIBS := -lskarnet
diff --git a/package/info b/package/info
index 2ff5910..5a2157b 100644
--- a/package/info
+++ b/package/info
@@ -1,4 +1,4 @@
 package=execline
-version=2.9.0.2
+version=2.9.1.0
 category=admin
 package_macro_name=EXECLINE
diff --git a/package/modes b/package/modes
index 17c52ce..78671a7 100644
--- a/package/modes
+++ b/package/modes
@@ -6,6 +6,7 @@ dollarat		0755
 elgetopt		0755
 elgetpositionals	0755
 elglob			0755
+eltest			0755
 execline-cd		0755
 execline-umask		0755
 emptyenv		0755
diff --git a/package/targets.mak b/package/targets.mak
index a0e5970..0b41b79 100644
--- a/package/targets.mak
+++ b/package/targets.mak
@@ -7,6 +7,7 @@ dollarat \
 elgetopt \
 elgetpositionals \
 elglob \
+eltest \
 emptyenv \
 envfile \
 exec \
diff --git a/src/execline/deps-exe/eltest b/src/execline/deps-exe/eltest
new file mode 100644
index 0000000..e7187fe
--- /dev/null
+++ b/src/execline/deps-exe/eltest
@@ -0,0 +1 @@
+-lskarnet
diff --git a/src/execline/eltest.c b/src/execline/eltest.c
new file mode 100644
index 0000000..b14318b
--- /dev/null
+++ b/src/execline/eltest.c
@@ -0,0 +1,505 @@
+/* ISC license. */
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <regex.h>
+
+#include <skalibs/posixplz.h>
+#include <skalibs/types.h>
+#include <skalibs/strerr.h>
+#include <skalibs/djbunix.h>
+
+enum opnum
+{
+  T_NOT,
+  T_AND,
+  T_OR,
+  T_LEFTP,
+  T_RIGHTP,
+  T_BLOCK,
+  T_CHAR,
+  T_DIR,
+  T_EXIST,
+  T_REGULAR,
+  T_SGID,
+  T_SYMLINK,
+  T_STICKY,
+  T_NONZERO,
+  T_FIFO,
+  T_READABLE,
+  T_NONZEROFILE,
+  T_TERM,
+  T_SUID,
+  T_WRITABLE,
+  T_EXECUTABLE,
+  T_ZERO,
+  T_EUID,
+  T_EGID,
+  T_SOCKET,
+  T_MODIFIED,
+  T_NEWER,
+  T_OLDER,
+  T_DEVINO,
+  T_STREQUAL,
+  T_STRNEQUAL,
+  T_STRLESSER,
+  T_STRLESSERE,
+  T_STRGREATER,
+  T_STRGREATERE,
+  T_NUMEQUAL,
+  T_NUMNEQUAL,
+  T_NUMGREATER,
+  T_NUMGREATERE,
+  T_NUMLESSER,
+  T_NUMLESSERE,
+  T_ENV,
+  T_MATCH
+} ;
+
+struct token
+{
+  char const *string ;
+  enum opnum op ;
+  unsigned int type ;
+} ;
+
+struct node
+{
+  enum opnum op ;
+  unsigned int type ;
+  unsigned int arg1 ;
+  unsigned int arg2 ;
+  char const *data ;
+} ;
+
+static unsigned int lex (struct node *tree, char const *const *argv)
+{
+  static struct token const tokens[46] =
+  {
+    { "-n", T_NONZERO, 2 },
+    { "-z", T_ZERO, 2 },
+    { "=", T_STREQUAL, 3 },
+    { "!=", T_STRNEQUAL, 3 },
+    { "-eq", T_NUMEQUAL, 3 },
+    { "-ne", T_NUMNEQUAL, 3 },
+    { "-gt", T_NUMGREATER, 3 },
+    { "-ge", T_NUMGREATERE, 3 },
+    { "-lt", T_NUMLESSER, 3 },
+    { "-le", T_NUMLESSERE, 3 },
+    { "-f", T_REGULAR, 2 },
+    { "-h", T_SYMLINK, 2 },
+    { "-L", T_SYMLINK, 2 },
+    { "-e", T_EXIST, 2 },
+    { "-k", T_STICKY, 2 },
+    { "-a", T_AND, 7 },
+    { "-o", T_OR, 8 },
+    { "!", T_NOT, 6 },
+    { "(", T_LEFTP, 4 },
+    { ")", T_RIGHTP, 5 },
+    { "-b", T_BLOCK, 2 },
+    { "-c", T_CHAR, 2 },
+    { "-d", T_DIR, 2 },
+    { "-g", T_SGID, 2 },
+    { "-p", T_FIFO, 2 },
+    { "-r", T_READABLE, 2 },
+    { "-s", T_NONZEROFILE, 2 },
+    { "-t", T_TERM, 2 },
+    { "-u", T_SUID, 2 },
+    { "-w", T_WRITABLE, 2 },
+    { "-x", T_EXECUTABLE, 2 },
+    { "-O", T_EUID, 2 },
+    { "-U", T_EUID, 2 },
+    { "-G", T_EGID, 2 },
+    { "-S", T_SOCKET, 2 },
+    { "-N", T_MODIFIED, 2 },
+    { "-nt", T_NEWER, 3 },
+    { "-ot", T_OLDER, 3 },
+    { "-ef", T_DEVINO, 3 },
+    { "<", T_STRLESSER, 3 },
+    { "<=", T_STRLESSERE, 3 },
+    { ">", T_STRGREATER, 3 },
+    { ">=", T_STRGREATERE, 3 },
+    { "-v", T_ENV, 2 },
+    { "=~", T_MATCH, 3 },
+    { 0, 0, 0 }
+  } ;
+  unsigned int pos = 0 ;
+
+  for (; argv[pos] ; pos++)
+  {
+    unsigned int i = 0 ;
+    tree[pos].data = argv[pos] ;
+    for (i = 0 ; tokens[i].string ; i++)
+      if (!strcmp(argv[pos], tokens[i].string))
+      {
+        tree[pos].op = tokens[i].op ;
+        tree[pos].type = tokens[i].type ;
+        break ;
+      }
+    if (!tokens[i].string)
+    {
+      tree[pos].op = T_NONZERO ;
+      tree[pos].type = 0 ;
+      tree[pos].arg1 = pos ;
+      if (*(argv[pos]) == '\\') tree[pos].data++ ;
+    }
+  }
+  return pos ;
+}
+
+static unsigned int parse (struct node *tree, unsigned int n)
+{
+  static char const table[9][13] =
+  {
+    "xssssxsssxxxx",
+    "xxxxxaxxxxxxx",
+    "xsxxsxsssxxxx",
+    "sxxxxxxxxxxxx",
+    "xsxxsxsssxxxx",
+    "nxxxxNxxxAOEs",
+    "xsxxsxsssxxxx",
+    "nxxxxNxxxAsxx",
+    "nxxxxNxxxAOsx"
+  } ;
+
+  unsigned int stack[n+2] ;
+  unsigned int sp = 0, pos = 0 ;
+  int cont = 1 ;
+
+  stack[0] = n+1 ;
+  tree[n].type = 5 ; /* add ) for the final reduce */
+  tree[n+1].type = 1 ; /* add EOF */
+
+  while (cont)
+  {
+    switch (table[tree[pos].type][tree[stack[sp]].type])
+    {
+      case 'x' : /* error */
+      {
+        char fmt[UINT_FMT] ;
+        fmt[uint_fmt(fmt, pos)] = 0 ;
+        strerr_dief2x(100, "parse error at argument ", fmt) ;
+        break ;
+      }
+      case 'a' : /* accept */
+      {
+        cont = 0 ;
+        break ;
+      }
+      case 's' : /* shift */
+      {
+        stack[++sp] = pos++ ;
+        break ;
+      }
+      case 'n' : /* reduce -> expr without nots, from atom */
+      {
+        switch (tree[stack[sp-1]].type)
+        {
+          case 2 :
+          {
+            tree[stack[sp-1]].arg1 = stack[sp] ;
+            sp-- ;
+            break ;
+          }
+          case 3 :
+          {
+            tree[stack[sp-1]].arg1 = stack[sp-2] ;
+            tree[stack[sp-1]].arg2 = stack[sp] ;
+            stack[sp-2] = stack[sp-1] ;
+            sp -= 2 ;
+            break ;
+          }
+          /* default : assert: its a zero */
+        }
+        tree[stack[sp]].type = 9 ;
+        while (tree[stack[sp-1]].type == 6)
+        {
+          tree[stack[sp-1]].type = 9 ;
+          tree[stack[sp-1]].arg1 = stack[sp] ;
+          sp-- ;
+        }
+        break ;
+      }
+      case 'N' : /* reduce -> expr without nots, from expr */
+      {
+        if (tree[stack[sp-2]].type != 4)
+        {
+          char fmt[UINT_FMT] ;
+          fmt[uint_fmt(fmt, pos)] = 0 ;
+          strerr_dief2x(100, "parse error: bad right parenthesis at argument ", fmt) ;
+        }
+        stack[sp-2] = stack[sp-1] ;
+        sp -= 2 ;
+        tree[stack[sp]].type = 9 ;
+        while (tree[stack[sp-1]].type == 6)
+        {
+          tree[stack[sp-1]].type = 9 ;
+          tree[stack[sp-1]].arg1 = stack[sp] ;
+          sp-- ;
+        }
+        break ;
+      }
+      case 'A' : /* reduce -> exprs without ands */
+      {
+        if (tree[stack[sp-1]].type == 7)
+        {
+          tree[stack[sp-1]].arg1 = stack[sp-2] ;
+          tree[stack[sp-1]].arg2 = stack[sp] ;
+          stack[sp-2] = stack[sp-1] ;
+          sp -= 2 ;
+        }
+        tree[stack[sp]].type = 10 ;
+        break ;
+      }
+      case 'O' : /* reduce -> expr without ors */
+      {
+        if (tree[stack[sp-1]].type == 8)
+        {
+          tree[stack[sp-1]].arg1 = stack[sp-2] ;
+          tree[stack[sp-1]].arg2 = stack[sp] ;
+          stack[sp-2] = stack[sp-1] ;
+          sp -= 2 ;
+        }
+        tree[stack[sp]].type = 11 ;
+        break ;
+      }
+      case 'E' : /* reduce -> expr */
+      {
+        tree[stack[sp]].type = 12 ;
+        break ;
+      }
+      default : /* can't happen */
+        strerr_dief1x(101, "internal error, please submit a bug-report.") ;
+    }
+  }
+  if (sp != 2) strerr_dief1x(100, "parse error: too many left parentheses") ;
+  return stack[1] ;
+}
+
+static int run (struct node const *tree, unsigned int root)
+{
+  switch (tree[root].op)
+  {
+    case T_NOT :
+      return !run(tree, tree[root].arg1) ;
+    case T_AND :
+      return run(tree, tree[root].arg1) && run(tree, tree[root].arg2) ;
+    case T_OR :
+      return run(tree, tree[root].arg1) || run(tree, tree[root].arg2) ;
+    case T_EXIST :
+      return access(tree[tree[root].arg1].data, F_OK) == 0 ;
+    case T_BLOCK :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISBLK(st.st_mode) ;
+    }
+    case T_CHAR :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISCHR(st.st_mode) ;
+    }
+    case T_DIR :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISDIR(st.st_mode) ;
+    }
+    case T_REGULAR :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISREG(st.st_mode) ;
+    }
+    case T_FIFO :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISFIFO(st.st_mode) ;
+    }
+    case T_SOCKET :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISSOCK(st.st_mode) ;
+    }
+    case T_SYMLINK :
+    {
+      struct stat st ;
+      if (lstat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return S_ISLNK(st.st_mode) ;
+    }
+    case T_SGID :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return (st.st_mode & S_ISGID) ;
+    }
+    case T_SUID :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return (st.st_mode & S_ISUID) ;
+    }
+    case T_STICKY :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return (st.st_mode & S_ISVTX) ;
+    }
+    case T_NONZEROFILE :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return (st.st_size > 0) ;
+    }
+    case T_MODIFIED :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return (st.st_mtime > st.st_atime) ;
+    }
+    case T_EUID :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return st.st_uid == geteuid() ;
+    }
+    case T_EGID :
+    {
+      struct stat st ;
+      if (stat(tree[tree[root].arg1].data, &st) == -1) return 0 ;
+      return st.st_gid == getegid() ;
+    }
+    case T_READABLE :
+      return access(tree[tree[root].arg1].data, R_OK) == 0 ;
+    case T_WRITABLE :
+      return access(tree[tree[root].arg1].data, W_OK) == 0 ;
+    case T_EXECUTABLE :
+      return access(tree[tree[root].arg1].data, X_OK) == 0 ;
+    case T_NEWER :
+    {
+      struct stat st1, st2 ;
+      if (stat(tree[tree[root].arg1].data, &st1) == -1) return 0 ;
+      if (stat(tree[tree[root].arg2].data, &st2) == -1) return 1 ;
+      return st1.st_mtime > st2.st_mtime ;
+    }
+    case T_OLDER :
+    {
+      struct stat st1, st2 ;
+      if (stat(tree[tree[root].arg1].data, &st1) == -1) return 1 ;
+      if (stat(tree[tree[root].arg2].data, &st2) == -1) return 0 ;
+      return st1.st_mtime < st2.st_mtime ;
+    }
+    case T_DEVINO :
+    {
+      struct stat st1, st2 ;
+      if (stat(tree[tree[root].arg1].data, &st1) == -1) return 0 ;
+      if (stat(tree[tree[root].arg2].data, &st2) == -1) return 1 ;
+      return (st1.st_dev == st2.st_dev) && (st1.st_ino == st2.st_ino) ;
+    }
+    case T_TERM :
+    {
+      unsigned int fd ;
+      if (!uint0_scan(tree[tree[root].arg1].data, &fd))
+        strerr_dief2x(100, tree[root].data, " requires an integer argument") ;
+      return isatty(fd) ;
+    }
+    case T_NONZERO :
+      return tree[tree[root].arg1].data[0] ;
+    case T_ZERO :
+      return !tree[tree[root].arg1].data[0] ;
+    case T_STREQUAL :
+      return !strcmp(tree[tree[root].arg1].data, tree[tree[root].arg2].data) ;
+    case T_STRNEQUAL :
+      return !!strcmp(tree[tree[root].arg1].data, tree[tree[root].arg2].data) ;
+    case T_STRLESSER :
+      return strcmp(tree[tree[root].arg1].data, tree[tree[root].arg2].data) < 0 ;
+    case T_STRLESSERE :
+      return strcmp(tree[tree[root].arg1].data, tree[tree[root].arg2].data) <= 0 ;
+    case T_STRGREATER :
+      return strcmp(tree[tree[root].arg1].data, tree[tree[root].arg2].data) > 0 ;
+    case T_STRGREATERE :
+      return strcmp(tree[tree[root].arg1].data, tree[tree[root].arg2].data) >= 0 ;
+    case T_NUMEQUAL :
+    {
+      int n1, n2 ;
+      if (!int_scan(tree[tree[root].arg1].data, &n1)
+       || !int_scan(tree[tree[root].arg2].data, &n2))
+        goto errorint ;
+      return n1 == n2 ;
+    }
+    case T_NUMNEQUAL :
+    {
+      int n1, n2 ;
+      if (!int_scan(tree[tree[root].arg1].data, &n1)
+       || !int_scan(tree[tree[root].arg2].data, &n2))
+        goto errorint ;
+      return n1 != n2 ;
+    }
+    case T_NUMGREATER :
+    {
+      int n1, n2 ;
+      if (!int_scan(tree[tree[root].arg1].data, &n1)
+       || !int_scan(tree[tree[root].arg2].data, &n2))
+        goto errorint ;
+      return n1 > n2 ;
+    }
+    case T_NUMGREATERE :
+    {
+      int n1, n2 ;
+      if (!int_scan(tree[tree[root].arg1].data, &n1)
+       || !int_scan(tree[tree[root].arg2].data, &n2))
+        goto errorint ;
+      return n1 >= n2 ;
+    }
+    case T_NUMLESSER :
+    {
+      int n1, n2 ;
+      if (!int_scan(tree[tree[root].arg1].data, &n1)
+       || !int_scan(tree[tree[root].arg2].data, &n2))
+        goto errorint ;
+      return n1 < n2 ;
+    }
+    case T_NUMLESSERE :
+    {
+      int n1, n2 ;
+      if (!int_scan(tree[tree[root].arg1].data, &n1)
+       || !int_scan(tree[tree[root].arg2].data, &n2))
+        goto errorint ;
+      return n1 <= n2 ;
+    }
+    case T_ENV :
+      return !!getenv(tree[tree[root].arg1].data) ;
+    case T_MATCH :
+    {
+      regex_t re ;
+      int r = skalibs_regcomp(&re, tree[tree[root].arg2].data, REG_EXTENDED | REG_NOSUB) ;
+      if (r)
+      {
+        char buf[256] ;
+        regerror(r, &re, buf, 256) ;
+        strerr_diefu4x(r == REG_ESPACE ? 111 : 100, "compile ", tree[tree[root].arg2].data, " into a regular expression: ", buf) ;
+      }
+      r = regexec(&re, tree[tree[root].arg1].data, 0, 0, 0) ;
+      regfree(&re) ;
+      return !r ;
+    }
+    default:
+      strerr_dief1x(101, "operation not implemented") ;
+  }
+
+errorint:
+  strerr_dief2x(100, tree[root].data, " requires integer arguments") ;
+}
+
+int main (int argc, char const *const *argv)
+{
+  struct node tree[argc+2] ;
+  if (argc <= 1) return 1 ;
+  PROG = "eltest" ;
+  return !run(tree, parse(tree, lex(tree, argv+1))) ;
+}