Whamcloud - gitweb
EX-4539 lipe: add lipe_find3
authorJohn L. Hammond <jhammond@whamcloud.com>
Tue, 8 Feb 2022 14:20:11 +0000 (08:20 -0600)
committerJohn L. Hammond <jhammond@whamcloud.com>
Thu, 10 Mar 2022 17:25:41 +0000 (17:25 +0000)
Add a lipe_find3 wrapper around the lipe_scan3 scanner and test script
sanity-lipe-find3.sh.

Test-Parameters: trivial testlist=sanity-lipe-find3
Signed-off-by: John L. Hammond <jhammond@whamcloud.com>
Change-Id: I2259170e8b71a94394009aeaf9878a17c2a3fa6d
Reviewed-on: https://review.whamcloud.com/46417
Tested-by: jenkins <devops@whamcloud.com>
22 files changed:
lipe/Makefile.am
lipe/configure.ac
lipe/lipe.spec.in
lipe/src/lipe_find3/.gitignore [new file with mode: 0644]
lipe/src/lipe_find3/Makefile.am [new file with mode: 0644]
lipe/src/lipe_find3/NOTES [new file with mode: 0644]
lipe/src/lipe_find3/bison-3.8.2.tar.gz [new file with mode: 0644]
lipe/src/lipe_find3/find-actions.txt [new file with mode: 0644]
lipe/src/lipe_find3/find-tests.txt [new file with mode: 0644]
lipe/src/lipe_find3/lf3_debug.h [new file with mode: 0644]
lipe/src/lipe_find3/lf3_lexer.h [new file with mode: 0644]
lipe/src/lipe_find3/lf3_lexer.l [new file with mode: 0644]
lipe/src/lipe_find3/lf3_parse.y [new file with mode: 0644]
lipe/src/lipe_find3/lf3_parse_format.c [new file with mode: 0644]
lipe/src/lipe_find3/lf3_parse_format.h [new file with mode: 0644]
lipe/src/lipe_find3/lf3_printf.c [new file with mode: 0644]
lipe/src/lipe_find3/lf3_printf.h [new file with mode: 0644]
lipe/src/lipe_find3/lipe/find.scm [new file with mode: 0644]
lipe/src/lipe_find3/lipe_scan3_mock.scm [new file with mode: 0755]
lipe/src/lipe_find3/xmalloc.h [new file with mode: 0644]
lustre/tests/Makefile.am
lustre/tests/sanity-lipe-find3.sh [new file with mode: 0644]

index b0f1a2b..86f8de0 100644 (file)
@@ -1,4 +1,4 @@
-SUBDIRS = src src/lipe_scan3 pybuild pylipe .
+SUBDIRS = src src/lipe_find3 src/lipe_scan3 pybuild pylipe .
 
 build_dir = `pwd`/build
 rpmbuild_opt =
index c6f441d..239d2ba 100644 (file)
@@ -313,6 +313,7 @@ AC_SUBST(ac_configure_args)
 AC_CONFIG_FILES([Makefile
                  lipe.spec
                  src/Makefile
+                src/lipe_find3/Makefile
                 src/lipe_scan3/Makefile
                  pybuild/Makefile
                  pylipe/Makefile])
index de3a2bc..cf122e6 100644 (file)
@@ -188,11 +188,13 @@ cp \
        src/ext4_inode2path \
        src/ldumpstripe \
        src/lfill \
+       src/lipe_find3/lipe_find3 \
        src/lipe_scan \
        src/lipe_scan2 \
        src/lipe_scan3/lipe_scan3 \
        $RPM_BUILD_ROOT%{_bindir}
 
+cp -a src/lipe_find3/lipe $RPM_BUILD_ROOT%{guile_site_dir}
 cp -a src/lipe_scan3/lipe $RPM_BUILD_ROOT%{guile_site_dir}
 
 %if %{with laudit}
@@ -327,6 +329,7 @@ rm -rf $RPM_BUILD_ROOT
 %{_bindir}/lipe_delete
 %{_bindir}/lipe_find
 %{_bindir}/lipe_find2
+%{_bindir}/lipe_find3
 %{_bindir}/lipe_launch
 %{_bindir}/lipe_purge
 %{_bindir}/lipe_scan
diff --git a/lipe/src/lipe_find3/.gitignore b/lipe/src/lipe_find3/.gitignore
new file mode 100644 (file)
index 0000000..28e88e4
--- /dev/null
@@ -0,0 +1,13 @@
+*~
+*.gcda
+*.gcno
+*.gcov
+*.o
+*.tab.c
+*.tab.h
+lf3_convert_expr
+lf3_lexer.c
+lf3_printf
+lipe_find3
+bison-3.8.2/
+opt/
diff --git a/lipe/src/lipe_find3/Makefile.am b/lipe/src/lipe_find3/Makefile.am
new file mode 100644 (file)
index 0000000..29336b8
--- /dev/null
@@ -0,0 +1,48 @@
+AUTOMAKE_OPTIONS = -Wall foreign
+ACLOCAL_AMFLAGS = ${ALOCAL_FLAGS}
+LEX = flex
+OPT := $(shell pwd)/opt
+YACC = $(OPT)/bin/bison
+
+if BUILD_SERVER
+
+# BUILT_SOURCES = lf3_lexer.c lf3_parse.tab.c lf3_parse.tab.h
+
+EXTRA_DIST = \
+       bison-3.8.2.tar.gz \
+       lf3_lexer.l \
+       lf3_parse.y \
+       lipe/find.scm
+
+bin_PROGRAMS = lipe_find3
+
+lipe_find3_CPPFLAGS = -D_GNU_SOURCE -I .. -I ../.. -include config.h
+lipe_find3_CFLAGS = -Wall -Werror -g -coverage
+lipe_find3_SOURCES = \
+        ../lipe_version.c \
+        ../lipe_version.h \
+       ../list.h \
+       lf3_debug.h \
+       lf3_lexer.h \
+       lf3_parse_format.c \
+       lf3_parse_format.h \
+       lf3_printf.c \
+       lf3_printf.h \
+       xmalloc.h
+
+nodist_lipe_find3_SOURCES = \
+       lf3_lexer.c \
+       lf3_parse.tab.c \
+       lf3_parse.tab.h
+
+# Need to callout the lf3_parse.tab.h dependency.
+lf3_lexer.c: lf3_lexer.l lf3_parse.tab.h
+       $(LEX) $(LFLAGS) --outfile=$@ $^
+
+lf3_parse.tab.c lf3_parse.tab.h: lf3_parse.y $(YACC)
+       $(YACC) $(YFLAGS) --defines lf3_parse.y
+
+$(YACC): bison-3.8.2.tar.gz
+       (tar -xzf bison-3.8.2.tar.gz && cd bison-3.8.2 && ./configure --prefix=$(OPT) && make install)
+
+endif # BUILD_SERVER
diff --git a/lipe/src/lipe_find3/NOTES b/lipe/src/lipe_find3/NOTES
new file mode 100644 (file)
index 0000000..abc5356
--- /dev/null
@@ -0,0 +1,148 @@
+TODO "{fid}" in exec?
+TODO dev, rdev
+TODO -empty
+TODO Check that format is MT safe.
+TODO Check that system* is MT safe.
+TODO dynamic-wind and MT.
+TODO scm exceptions generate errors with no progname prefix.
+TODO scanning and encryption
+TODO xargs batch limits needed?
+TODO crtime?
+TODO symlinks need readlink in lipe_scan3
+TODO print-json attrs --list-json-attrs
+
+TODO fprintf
+       \a \b \f \n \r \t \v \0 \\
+       \OOO => \xXX
+
+       Field widths and precisions can be specified as with the
+       `printf' C function. Please note that many of the fields are
+       printed as %s rather than %d, and this may mean that flags
+       don't work as you might expect. This also means that the `-'
+       flag does work (it forces fields to be left-aligned).
+
+       Flags #, 0, -, ' ', +
+
+               '#' flag
+
+       \c     Stop printing from this format immediately and flush the output.
+
+       NI A `\' character followed by any other character is treated as an ordinary character, so they both are printed.
+       A '\' followed by and other char is invalid.
+
+       +      Date and time, separated by `+', for example `2004-04-28+22:22:05.0'.
+       This is a GNU extension.  The time is given in the  current  timezone
+       (which  may be affected by setting the TZ environment variable).  The
+       seconds field includes a fractional part.
+
+
+       %% a literal percent sign
+       %a atime in ctime() format
+       %Ak (k is @ or a strftime directive)
+
+       %b block count
+       %c ctime in ctime() format
+       %Ck ...
+NI     %d File's depth in the directory tree; 0 means the file is a starting-point.
+NI     %D %D The device number on which the file exists (st_dev in decimal)
+       %f File's name with any leading directories removed (only the last element)
+NI     %F Type of the filesystem the file is on; this value can be used for -fstype.
+       %g File's group name, or numeric group ID if group has no name.
+       %G numeric gid
+
+       %h Leading directories of file's name (all but the last
+          element). If the file name contains no slashes (since it is
+          in the current directory) the %h specifier expands to `.'.
+
+               (call-with-relative-path dirname)
+
+??     %H     Starting-point under which file was found.
+
+               (client-mount-point)
+
+       %i ino decimal
+
+       %k disk space used in 1KB blocks. (rounded up)
+DEFER  %l symlink target or empty string if not symlink
+
+       %m File's permission bits (in octal). This option uses the
+          `traditional' numbers which most Unix implementations use,
+          but if your particular implementation uses an unusual
+          ordering of octal permissions bits, you will see a dif‐
+          ference between the actual value of the file's mode and the
+          output of %m.  Normally you will want to have a leading
+          zero on this number, and to do this, you should use the #
+          flag (as in, for example, `%#m').
+
+          k:~# find /mnt/lustre -name f0 -printf '%m\n'
+          644
+          k:~# find /mnt/lustre -name f0 -printf '%#m\n'
+          0644
+          k:~# find /mnt/lustre -name f0 -printf '%03m\n'
+          644
+          k:~# find /mnt/lustre -name f0 -printf '%06m\n'
+          000644
+          k:~# find /mnt/lustre -name f0 -printf '%02m\n'
+          644
+
+       %M File's  permissions  (in  symbolic form, as for ls)
+       %n     Number of hard links to file.
+       %p     File's name.
+
+       # find /mnt/lustre -name f9 -printf '%p\n'
+       /mnt/lustre/sanity/f9
+       # cd /mnt/lustre && find . -name f9 -printf '%p\n'
+       ./sanity/f9
+
+       %P File's name with the name of the starting-point under which it was found removed
+
+       k:~# find /mnt/lustre -name f9 -printf '%P\n'
+       sanity/f9
+       k:~# cd /mnt/lustre && find . -name f9 -printf '%P\n'
+       sanity/f9
+
+               (relative-path)
+
+       %S     File's  sparseness.   This is calculated as (BLOCKSIZE*st_blocks / st_size).
+       The exact value you will get for an ordinary file of  a  certain  length  is
+                     system-dependent.  However, normally sparse files will have values less than
+                     1.0, and files which use indirect blocks may have a value which  is  greater
+                     than 1.0.  In general the number of blocks used by a file is file system de‐
+                     pendent.  The value used for BLOCKSIZE is system-dependent, but  is  usually
+                     512  bytes.   If  the file size is zero, the value printed is undefined.  On
+                     systems which lack support for st_blocks, a file's sparseness is assumed  to
+                     be 1.0.
+
+       size == 0 => sparseness = 1
+
+
+Extensions
+       %{fid} FID as string w/o braces
+       %{device-name} "fs0a42-MDT0007"
+       %{device-path} "/dev/mapper/mds1_flakey"
+       %{client-mount-path}
+
+TODO Regex:
+       Try compling the regex first for error messages.
+
+By default, Guile supports POSIX extended regular expressions. That
+means that the characters ‘(’, ‘)’, ‘+’ and ‘?’ are special, and must
+be escaped if you wish to match the literal characters and there is no
+support for “non-greedy” variants of ‘*’, ‘+’ or ‘?’.
+
+regexp/icase
+regexp/basic
+regexp/extended
+
+(define %LEC:regex-42 (make-regexp pattern regexp/basic regexp/extended))
+
+pattern flags
+
+(define %LEC:match-fname-107
+  (let* ((%LEC:match-fname-108
+           (lambda (str)
+             (fnmatch? ,pattern str ,flags))))
+    (lambda () (any %LEC:match-fname-108 (paths)))))
+
+regexp/icase
+(use-modules (ice-9 regex))
diff --git a/lipe/src/lipe_find3/bison-3.8.2.tar.gz b/lipe/src/lipe_find3/bison-3.8.2.tar.gz
new file mode 100644 (file)
index 0000000..127db12
Binary files /dev/null and b/lipe/src/lipe_find3/bison-3.8.2.tar.gz differ
diff --git a/lipe/src/lipe_find3/find-actions.txt b/lipe/src/lipe_find3/find-actions.txt
new file mode 100644 (file)
index 0000000..98df9b5
--- /dev/null
@@ -0,0 +1,360 @@
+DONE
+       -delete
+              Delete  files;  true if removal succeeded.  If the removal failed, an error message
+              is issued.  If -delete fails, find's exit status will be nonzero (when  it  eventu‐
+              ally exits).  Use of -delete automatically turns on the `-depth' option.
+
+              Warnings: Don't forget that the find command line is evaluated as an expression, so
+              putting -delete first will make find try to delete everything  below  the  starting
+              points  you  specified.   When testing a find command line that you later intend to
+
+              use with -delete, you should explicitly specify -depth in order to avoid later sur‐
+              prises.  Because -delete implies -depth, you cannot usefully use -prune and -delete
+              together.
+
+              Together with the -ignore_readdir_race option,  find  will  ignore  errors  of  the
+              -delete  action in the case the file has disappeared since the parent directory was
+              read: it will not output an error diagnostic, and the return code  of  the  -delete
+              action will be true.
+
+       -exec command ;
+              Execute command; true if 0 status is returned.  All following arguments to find are
+              taken to be arguments to the command until an argument consisting of `;' is encoun‐
+              tered.  The string `{}' is replaced by the current file name being processed every‐
+              where it occurs in the arguments to the command, not just in arguments where it  is
+              alone,  as  in some versions of find.  Both of these constructions might need to be
+              escaped (with a `\') or quoted to protect them from expansion by  the  shell.   See
+              the  EXAMPLES  section  for examples of the use of the -exec option.  The specified
+              command is run once for each matched file.  The command is executed in the starting
+              directory.   There  are  unavoidable security problems surrounding use of the -exec
+              action; you should use the -execdir option instead.
+
+       -exec command {} +
+              This variant of the -exec action runs the specified command on the selected  files,
+              but  the command line is built by appending each selected file name at the end; the
+              total number of invocations of the command will be much less  than  the  number  of
+              matched  files.   The  command line is built in much the same way that xargs builds
+              its command lines.  Only one instance of `{}' is allowed within  the  command,  and
+              (when  find  is being invoked from a shell) it should be quoted (for example, '{}')
+              to protect it from interpretation by shells.  The command is executed in the start‐
+              ing  directory.   If  any  invocation with the `+' form returns a non-zero value as
+              exit status, then find returns a non-zero exit status.  If find encounters  an  er‐
+              ror,  this  can sometimes cause an immediate exit, so some pending commands may not
+              be run at all.  This variant of -exec always returns true.
+
+       -fprint file
+              True; print the full file name into file file.  If file does not exist when find is
+              run,  it  is created; if it does exist, it is truncated.  The file names `/dev/std‐
+              out' and `/dev/stderr' are handled specially; they refer to the standard output and
+              standard  error  output,  respectively.  The output file is always created, even if
+              the predicate is never matched.  See the UNUSUAL FILENAMES section for  information
+              about how unusual characters in filenames are handled.
+
+       -fprint0 file
+              True;  like -print0 but write to file like -fprint.  The output file is always cre‐
+              ated, even if the predicate is never matched.  See the  UNUSUAL  FILENAMES  section
+              for information about how unusual characters in filenames are handled.
+
+       -fprintf file format
+              True;  like -printf but write to file like -fprint.  The output file is always cre‐
+              ated, even if the predicate is never matched.  See the  UNUSUAL  FILENAMES  section
+              for information about how unusual characters in filenames are handled.
+
+       -print True;  print  the full file name on the standard output, followed by a newline.  If
+              you are piping the output of find into another program and there  is  the  faintest
+              possibility  that  the  files  which you are searching for might contain a newline,
+              then you should seriously consider using the -print0 option instead of -print.  See
+              the UNUSUAL FILENAMES section for information about how unusual characters in file‐
+              names are handled.
+
+       -print0
+              True; print the full file name on the standard output, followed by a null character
+              (instead  of  the newline character that -print uses).  This allows file names that
+              contain newlines or other types of white space to be correctly interpreted by  pro‐
+              grams  that  process  the find output.  This option corresponds to the -0 option of
+              xargs.
+
+PARTIALLY DONE
+       -printf
+               Not all format directives are implemented. Search for
+               LF3_EMIT_U(). Format width and precision is only
+               partially implemented.
+
+       -printf format
+              True; print format on the standard output, interpreting `\' escapes and `%'  direc‐
+              tives.   Field  widths and precisions can be specified as with the `printf' C func‐
+              tion.  Please note that many of the fields are printed as %s rather  than  %d,  and
+              this  may mean that flags don't work as you might expect.  This also means that the
+              `-' flag does work (it forces fields to be left-aligned).  Unlike  -print,  -printf
+              does not add a newline at the end of the string.  The escapes and directives are:
+
+              \a     Alarm bell.
+
+              \b     Backspace.
+
+              \c     Stop printing from this format immediately and flush the output.
+
+              \f     Form feed.
+
+              \n     Newline.
+
+              \r     Carriage return.
+
+              \t     Horizontal tab.
+
+              \v     Vertical tab.
+
+              \0     ASCII NUL.
+
+              \\     A literal backslash (`\').
+
+              \NNN   The character whose ASCII code is NNN (octal).
+
+              A  `\'  character followed by any other character is treated as an ordinary charac‐
+              ter, so they both are printed.
+
+              %%     A literal percent sign.
+
+              %a     File's last access time in the format returned by the C `ctime' function.
+
+              %Ak    File's last access time in the format specified by k, which is either `@' or
+                     a  directive  for  the C `strftime' function.  The possible values for k are
+                     listed below; some of them might not be available on  all  systems,  due  to
+                     differences in `strftime' between systems.
+
+                     @      seconds since Jan. 1, 1970, 00:00 GMT, with fractional part.
+
+                     Time fields:
+
+                     H      hour (00..23)
+
+                     I      hour (01..12)
+
+                     k      hour ( 0..23)
+
+                     l      hour ( 1..12)
+
+                     M      minute (00..59)
+
+                     p      locale's AM or PM
+
+                     r      time, 12-hour (hh:mm:ss [AP]M)
+
+                     S      Second (00.00 .. 61.00).  There is a fractional part.
+
+                     T      time, 24-hour (hh:mm:ss.xxxxxxxxxx)
+
+                     +      Date and time, separated by `+', for example `2004-04-28+22:22:05.0'.
+                            This is a GNU extension.  The time is given in the  current  timezone
+                            (which  may be affected by setting the TZ environment variable).  The
+                            seconds field includes a fractional part.
+
+                     X      locale's time representation (H:M:S).  The seconds field  includes  a
+                            fractional part.
+
+                     Z      time zone (e.g., EDT), or nothing if no time zone is determinable
+
+                     Date fields:
+
+                     a      locale's abbreviated weekday name (Sun..Sat)
+
+                     A      locale's full weekday name, variable length (Sunday..Saturday)
+
+                     b      locale's abbreviated month name (Jan..Dec)
+
+                     B      locale's full month name, variable length (January..December)
+
+                     c      locale's date and time (Sat Nov 04 12:02:33 EST 1989).  The format is
+                            the same as for ctime(3) and so to preserve compatibility  with  that
+                            format, there is no fractional part in the seconds field.
+
+                     d      day of month (01..31)
+
+                     D      date (mm/dd/yy)
+
+                     h      same as b
+
+                     j      day of year (001..366)
+
+                     m      month (01..12)
+
+                     U      week number of year with Sunday as first day of week (00..53)
+
+                     w      day of week (0..6)
+
+                     W      week number of year with Monday as first day of week (00..53)
+
+                     x      locale's date representation (mm/dd/yy)
+
+                     y      last two digits of year (00..99)
+
+                     Y      year (1970...)
+
+              %b     The  amount of disk space used for this file in 512-byte blocks.  Since disk
+                     space is allocated in multiples of the filesystem block size this is usually
+                     greater  than  %s/512,  but  it  can also be smaller if the file is a sparse
+                     file.
+
+              %c     File's last status change time in the format returned by the C `ctime' func‐
+                     tion.
+
+              %Ck    File's  last  status  change time in the format specified by k, which is the
+                     same as for %A.
+
+              %d     File's depth in the directory tree; 0 means the file is a starting-point.
+
+              %D     The device number on which the file  exists  (the  st_dev  field  of  struct
+                     stat), in decimal.
+
+              %f     File's name with any leading directories removed (only the last element).
+
+              %F     Type of the filesystem the file is on; this value can be used for -fstype.
+
+              %g     File's group name, or numeric group ID if the group has no name.
+
+              %G     File's numeric group ID.
+
+              %h     Leading  directories of file's name (all but the last element).  If the file
+                     name contains no slashes (since it is in the current directory) the %h spec‐
+                     ifier expands to `.'.
+
+              %H     Starting-point under which file was found.
+
+              %i     File's inode number (in decimal).
+
+              %k     The  amount  of  disk  space  used for this file in 1 KB blocks.  Since disk
+                     space is allocated in multiples of the filesystem block size this is usually
+                     greater  than  %s/1024,  but  it can also be smaller if the file is a sparse
+                     file.
+
+              %l     Object of symbolic link (empty string if file is not a symbolic link).
+
+              %m     File's permission bits (in octal).  This option uses the `traditional'  num‐
+                     bers which most Unix implementations use, but if your particular implementa‐
+                     tion uses an unusual ordering of octal permissions bits, you will see a dif‐
+                     ference  between  the  actual value of the file's mode and the output of %m.
+                     Normally you will want to have a leading zero on  this  number,  and  to  do
+                     this, you should use the # flag (as in, for example, `%#m').
+
+              %M     File's  permissions  (in  symbolic form, as for ls).  This directive is sup‐
+                     ported in findutils 4.2.5 and later.
+
+              %n     Number of hard links to file.
+
+              %p     File's name.
+
+              %P     File's name with the name of the starting-point under which it was found re‐
+                     moved.
+
+              %s     File's size in bytes.
+
+              %S     File's  sparseness.   This is calculated as (BLOCKSIZE*st_blocks / st_size).
+                     The exact value you will get for an ordinary file of  a  certain  length  is
+                     system-dependent.  However, normally sparse files will have values less than
+                     1.0, and files which use indirect blocks may have a value which  is  greater
+                     than 1.0.  In general the number of blocks used by a file is file system de‐
+                     pendent.  The value used for BLOCKSIZE is system-dependent, but  is  usually
+                     512  bytes.   If  the file size is zero, the value printed is undefined.  On
+                     systems which lack support for st_blocks, a file's sparseness is assumed  to
+                     be 1.0.
+
+              %t     File's  last modification time in the format returned by the C `ctime' func‐
+                     tion.
+
+              %Tk    File's last modification time in the format specified by  k,  which  is  the
+                     same as for %A.
+
+              %u     File's user name, or numeric user ID if the user has no name.
+
+              %U     File's numeric user ID.
+
+              %y     File's type (like in ls -l), U=unknown type (shouldn't happen)
+
+              %Y     File's  type (like %y), plus follow symlinks: `L'=loop, `N'=nonexistent, `?'
+                     for any other error when determining the type of the symlink target.
+
+              %Z     (SELinux only) file's security context.
+
+              %{ %[ %(
+                     Reserved for future use.
+
+              A `%' character followed by any other character is discarded, but the other charac‐
+              ter  is  printed  (don't  rely  on this, as further format characters may be intro‐
+              duced).  A `%' at the end of the format argument causes undefined  behaviour  since
+              there  is  no  following  character.   In some locales, it may hide your door keys,
+              while in others it may remove the final page from the novel you are reading.
+
+              The %m and %d directives support the # , 0 and + flags, but the other directives do
+              not,  even  if  they  print  numbers.  Numeric directives that do not support these
+              flags include G, U, b, D, k and n.  The `-' format flag is  supported  and  changes
+              the alignment of a field from right-justified (which is the default) to left-justi‐
+              fied.
+
+              See the UNUSUAL FILENAMES section for information about how unusual  characters  in
+              filenames are handled.
+
+       -quit  Exit immediately.  No child processes will be left running, but no more paths spec‐
+              ified  on  the command line will be processed.  For example, find /tmp/foo /tmp/bar
+              -print -quit will print only /tmp/foo.  Any command lines which have been built  up
+              with  -execdir  ... {} + will be invoked before find exits.  The exit status may or
+              may not be zero, depending on whether an error has already occurred.
+
+DEFERRED
+       -execdir command ;
+
+       -execdir command {} +
+              Like -exec, but the specified command is run from the subdirectory  containing  the
+              matched  file,  which  is not normally the directory in which you started find.  As
+              with -exec, the {} should be quoted if find is being invoked from a shell.  This  a
+              much  more secure method for invoking commands, as it avoids race conditions during
+              resolution of the paths to the matched files.  As with the -exec  action,  the  `+'
+              form  of  -execdir will build a command line to process more than one matched file,
+              but any given invocation of command will only list files that  exist  in  the  same
+              subdirectory.   If you use this option, you must ensure that your $PATH environment
+              variable does not reference `.'; otherwise, an attacker can run any  commands  they
+              like  by  leaving  an appropriately-named file in a directory in which you will run
+              -execdir.  The same applies to having entries in $PATH which are empty or which are
+              not  absolute  directory names.  If any invocation with the `+' form returns a non-
+              zero value as exit status, then find returns a non-zero exit status.  If  find  en‐
+              counters an error, this can sometimes cause an immediate exit, so some pending com‐
+              mands may not be run at all.  The result of the action depends on whether the +  or
+              the  ; variant is being used; -execdir command {} + always returns true, while -ex‐
+              ecdir command {} ; returns true only if command returns 0.
+
+       -fls file
+              True; like -ls but write to file like -fprint.  The output file is always  created,
+              even  if the predicate is never matched.  See the UNUSUAL FILENAMES section for in‐
+              formation about how unusual characters in filenames are handled.
+
+       -ls    True;  list  current  file in ls -dils format on standard output.  The block counts
+              are of 1 KB blocks, unless the environment  variable  POSIXLY_CORRECT  is  set,  in
+              which  case 512-byte blocks are used.  See the UNUSUAL FILENAMES section for infor‐
+              mation about how unusual characters in filenames are handled.
+
+       -ok command ;
+              Like -exec but ask the user first.  If the user agrees, run the command.  Otherwise
+              just  return  false.   If the command is run, its standard input is redirected from
+              /dev/null.
+
+              The response to the prompt is matched against a pair of regular expressions to  de‐
+              termine  if  it is an affirmative or negative response.  This regular expression is
+              obtained from the system if the `POSIXLY_CORRECT' environment variable is  set,  or
+              otherwise  from find's message translations.  If the system has no suitable defini‐
+              tion, find's own definition will be used.  In either case,  the  interpretation  of
+              the  regular  expression  itself  will  be  affected  by  the environment variables
+              'LC_CTYPE' (character classes) and 'LC_COLLATE' (character ranges  and  equivalence
+              classes).
+
+       -okdir command ;
+              Like  -execdir but ask the user first in the same way as for -ok.  If the user does
+              not agree, just return false.  If the command is run, its standard input  is  redi‐
+              rected from /dev/null.
+
+WON'T DO
+       -prune True; if the file is a directory, do not descend into it.  If -depth is given, then
+              -prune has no effect.  Because -delete implies  -depth,  you  cannot  usefully  use
+              -prune and -delete together.
+                For  example, to skip the directory `src/emacs' and all files and directories un‐
+              der it, and print the names of the other files found, do something like this:
+                        find . -path ./src/emacs -prune -o -print
+
diff --git a/lipe/src/lipe_find3/find-tests.txt b/lipe/src/lipe_find3/find-tests.txt
new file mode 100644 (file)
index 0000000..dbc2fa4
--- /dev/null
@@ -0,0 +1,184 @@
+DONE
+       -amin
+       -atime
+       -cmin
+       -ctime
+       -false
+       -gid
+       -inum
+       -links
+       -mmin
+       -mtime
+       -size Based on '(size)' which is based on ?. Needs clarificaiton.
+       -true
+       -type
+       -uid
+
+       -user string or UID but no [+-] prefix on either.
+       -group ...
+
+               # ls -ld /mnt/lustre/sanity
+               drwxr-xr-x 2 sanity sanity 4096 Oct 20 11:01 /mnt/lustre/sanity
+               # find /mnt/lustre -user sanity
+               /mnt/lustre/sanity
+               # find /mnt/lustre -user +sanity
+               find: ‘+sanity’ is not the name of a known user
+               # find /mnt/lustre -user -sanity
+               find: ‘-sanity’ is not the name of a known user
+               # id sanity
+               uid=500(sanity) gid=500(sanity) groups=500(sanity)
+               # find /mnt/lustre -user +500
+               find: ‘+500’ is not the name of a known user
+               # find /mnt/lustre -user -500
+               find: ‘-500’ is not the name of a known user
+               # find /mnt/lustre -user 500
+               /mnt/lustre/sanity
+
+       -iname pattern
+              Like -name, but the match is case insensitive.  For example, the patterns `fo*' and
+              `F??'  match  the  file names `Foo', `FOO', `foo', `fOo', etc.  The pattern `*foo*`
+              will also match a file called '.foobar'.
+
+       -ipath pattern
+              Like -path.  but the match is case insensitive.
+
+       -name pattern
+               Base  of  file  name  (the path with the leading directories removed) matches shell
+               pattern pattern.  Because the leading directories are removed, the file names
+               considered  for  a  match  with -name will never include a slash, so `-name a/b' will
+               never match anything (you probably need to use -path instead).  A warning is issued
+               if you try to do this, unless the environment variable POSIXLY_CORRECT is set.  The
+               metacharacters (`*', `?', and `[]') match a `.' at the start of the base name (this
+               is  a  change in findutils-4.2.2; see section STANDARDS CONFORMANCE below).  To ig‐
+               nore a directory and the files under it, use -prune rather than checking every file
+               in  the  tree;  see  an  example in the description of that action.  Braces are not
+               recognised as being special, despite the fact that some shells including Bash imbue
+               braces  with  a  special  meaning in shell patterns.  The filename matching is per‐
+               formed with the use of the fnmatch(3) library function.  Don't  forget  to  enclose
+               the pattern in quotes in order to protect it from expansion by the shell.
+
+       -path pattern
+              File name matches shell pattern pattern.  The metacharacters do not  treat  `/'  or
+              `.' specially; so, for example,
+                        find . -path "./sr*sc"
+              will print an entry for a directory called `./src/misc' (if one exists).  To ignore
+              a whole directory tree, use -prune rather than checking every  file  in  the  tree.
+              Note  that the pattern match test applies to the whole file name, starting from one
+              of the start points named on the command line.  It would only make sense to use  an
+              absolute path name here if the relevant start point is also an absolute path.  This
+              means that this command will never match anything:
+                        find bar -path /foo/bar/myfile -print
+              Find compares the -path argument with the concatenation of a directory name and the
+              base  name of the file it's examining.  Since the concatenation will never end with
+              a slash, -path arguments ending in a slash will match  nothing  (except  perhaps  a
+              start  point specified on the command line).  The predicate -path is also supported
+              by HP-UX find and is part of the POSIX 2008 standard.
+
+TODO
+       -perm mode
+               File's permission bits are exactly mode (octal or symbolic).  Since an exact  match
+               is required, if you want to use this form for symbolic modes, you may have to spec‐
+               ify a rather complex mode string.  For example `-perm g=w' will  only  match  files
+               which  have  mode  0020 (that is, ones for which group write permission is the only
+               permission set).  It is more likely that you will want to use the `/' or `-' forms,
+               for  example `-perm -g=w', which matches any file with group write permission.  See
+               the EXAMPLES section for some illustrative examples.
+
+       -perm -mode
+               All of the permission bits mode are set for the file.  Symbolic modes are  accepted
+               in this form, and this is usually the way in which you would want to use them.  You
+               must specify `u', `g' or `o' if you use a symbolic mode.  See the EXAMPLES  section
+               for some illustrative examples.
+
+       -perm /mode
+              Any  of the permission bits mode are set for the file.  Symbolic modes are accepted
+              in this form.  You must specify `u', `g' or `o' if you use a  symbolic  mode.   See
+              the EXAMPLES section for some illustrative examples.  If no permission bits in mode
+              are set, this test matches any file (the idea here is to be consistent with the be‐
+              haviour of -perm -000).
+
+       -fstype
+
+       -used n
+               File was last accessed n days after its status was last changed.
+
+       -iregex pattern
+               Like -regex, but the match is case insensitive.
+
+       -regex pattern
+              File  name  matches regular expression pattern.  This is a match on the whole path,
+              not a search.  For example, to match a file named `./fubar3', you can use the regu‐
+              lar  expression `.*bar.' or `.*b.*3', but not `f.*r3'.  The regular expressions un‐
+              derstood by find are by default Emacs Regular Expressions (except that `.'  matches
+              newline), but this can be changed with the -regextype option.
+
+DEFERRED -empty
+       File is empty and is either a regular file or a directory.
+
+        How to handle this for striped directories (parents and substripes)?
+
+
+DEFERRED: Things that depend on a reference user or (access()):
+       -executable
+               Matches files which are executable and directories
+               which are searchable (in a file name resolution sense)
+               by the current user. This takes into account access
+               control lists and other permissions artefacts which
+               the -perm test ignores. This test makes use of the
+               access(2) system call, and so can be fooled by NFS
+               servers which do UID mapping (or root-squashing),
+               since many systems implement access(2) in the client's
+               kernel and so cannot make use of the UID mapping
+               information held on the server. Because this test is
+               based only on the result of the access(2) system call,
+               there is no guarantee that a file for which this test
+               succeeds can actually be executed.
+       -readable
+               ...
+       -writable
+               ...
+
+DEFERRED
+       -nogroup
+               No group corresponds to file's numeric group ID.
+
+       -nouser
+               No user corresponds to file's numeric user ID.
+
+       Read passwd file, get uids build an alist
+
+       (define %LEC:nouser?
+         (let* ((%LEC:uid-list '(uid1 uid2 ...))
+                (%LEC:nouser-uid-alist (map (lambda (uid) (cons uid #f)) %LEC:uid-list))
+                (%LEC:nouser-uid-table (alist->hashv-table %LEC:nouser-uid-alist)))
+            (lambda ()
+             (hashv-ref %LEC:nouser-uid-table (uid) #t))))
+
+
+DEFERRED (Need a way to read non-fast symlinks in scanner)
+       -lname pattern
+              File is a symbolic link whose contents match shell pattern pattern.  The  metachar‐
+              acters  do  not treat `/' or `.' specially.  If the -L option or the -follow option
+              is in effect, this test returns false unless the symbolic link is broken.
+
+       -ilname pattern
+              Like  -lname,  but  the match is case insensitive.  If the -L option or the -follow
+              option is in effect, this test returns false unless the symbolic link is broken.
+
+       -xtype c
+              The same as -type unless the file is a symbolic link.  For symbolic links:  if  the
+              -H  or  -P option was specified, true if the file is a link to a file of type c; if
+              the -L option has been given, true if c is  `l'.   In  other  words,  for  symbolic
+              links, -xtype checks the type of the file that -type does not check.
+
+DEFERRED:
+       -context pattern
+              (SELinux only) Security context of the file matches glob pattern.
+
+WON'T DO:
+       Things that depend on a reference file.
+       -anewer
+       -cnewer
+       -mnewer
+       -newer
+       -samefile
diff --git a/lipe/src/lipe_find3/lf3_debug.h b/lipe/src/lipe_find3/lf3_debug.h
new file mode 100644 (file)
index 0000000..1c5ff2c
--- /dev/null
@@ -0,0 +1,41 @@
+#ifndef _LF3_DEBUG_H_
+#define _LF3_DEBUG_H_
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+extern int lf3_debug;
+extern const char *lf3_progname_;
+
+static inline const char *lf3_progname(void)
+{
+       return lf3_progname_ != NULL ? lf3_progname_ : program_invocation_short_name;
+}
+
+#define LF3_DEBUG(fmt, args...)                                                \
+       do {                                                            \
+               if (lf3_debug)                                          \
+                       fprintf(stderr, "%s: DEBUG %s:%d: "fmt,         \
+                               lf3_progname(), __func__, __LINE__, ##args); \
+       } while (0)
+
+#define LF3_DEBUG_B(x) LF3_DEBUG("%s = %s\n", #x, (x) ? "true" : "false")
+#define LF3_DEBUG_D(x) LF3_DEBUG("%s = %"PRIdMAX"\n", #x, (intmax_t)(x))
+#define LF3_DEBUG_P(x) LF3_DEBUG("%s = %p\n", #x, x)
+#define LF3_DEBUG_S(x) LF3_DEBUG("%s = '%s'\n", #x, x)
+#define LF3_DEBUG_U(x) LF3_DEBUG("%s = %"PRIuMAX"\n", #x, (uintmax_t)(x))
+#define LF3_DEBUG_X(x) LF3_DEBUG("%s = %"PRIxMAX"\n", #x, (uintmax_t)(x))
+
+#define LF3_FATAL(fmt, args...)                                                \
+       do {                                                            \
+               fprintf(stderr, "%s: FATAL: "fmt, lf3_progname(), ##args); \
+               exit(EXIT_FAILURE);                                     \
+       } while (0)
+
+/* TODO Make a nice error message with a caret. */
+
+#define LF3_FATAL_AT(index, fmt, args...)              \
+       LF3_FATAL("at argument %d: "fmt, index, ##args)
+
+#endif
diff --git a/lipe/src/lipe_find3/lf3_lexer.h b/lipe/src/lipe_find3/lf3_lexer.h
new file mode 100644 (file)
index 0000000..7154953
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef _L3F_LEXER_H_
+#define _L3F_LEXER_H_
+
+extern const char **lf3_arg;
+extern int lf3_arg_count;
+extern int lf3_arg_index;
+int lf3_lexer_init(const char **arg, int arg_index, int arg_count);
+
+int yylex(void);
+
+#endif /* _LF3_LEXER_H_ */
diff --git a/lipe/src/lipe_find3/lf3_lexer.l b/lipe/src/lipe_find3/lf3_lexer.l
new file mode 100644 (file)
index 0000000..8cf3003
--- /dev/null
@@ -0,0 +1,193 @@
+%option noinput
+%option nounput
+%{
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+#include "lf3_debug.h"
+#include "lf3_lexer.h"
+#include "lf3_parse.tab.h"
+
+const char **lf3_arg;
+int lf3_arg_count;
+int lf3_arg_index;
+
+static int expr_arg_begin;
+static int expr_arg_end;
+static int expr_arg_type;
+
+#define YY_USER_ACTION yylloc.first_line = lf3_arg_index;
+
+#define YY_INPUT(buf,result,max_size) EOF
+
+/* FIXME: Need an EOF rule to check for incomplete expressions.
+ *
+ * Notes:
+ *
+ * All tokens must end with a NUL (\0) char to properly handle empty
+ * arguments.
+ *
+ * INITIAL is the default start state to gather a new expression.
+ *
+ * ARG is the exclusive start state to gather arguments for normal expressions
+ * (-name foo, -print, -delete) with fixed argument counts.
+ *
+ * EXEC_ARG is the exclusive start state to gather arguments for the -exec action
+ * and is terminated by ';' or '+'.
+ */
+
+%}
+
+%x ARG
+%x EXEC_ARG
+
+%% /* rules */
+
+-(delete|print|print0|print-file-fid|print-self-fid|print-absolute-path|print-relative-path|print-json|quit)\0 {
+       LF3_DEBUG("nullary '%s'\n", yytext);
+       yylval.u_range[0] = lf3_arg_index;
+       yylval.u_range[1] = lf3_arg_index + 1;
+       return TOKEN_ACTION;
+}
+
+-(empty|executable|false|readable|true|writable)\0 {
+       LF3_DEBUG("nullary '%s'\n", yytext);
+       yylval.u_range[0] = lf3_arg_index;
+       yylval.u_range[1] = lf3_arg_index + 1;
+       return TOKEN_TEST;
+}
+
+-(amin|atime|cmin|ctime|gid|group|iname|inum|links|mmin|mtime|name|path|perm|projid|size|type|uid|user)\0 {
+       LF3_DEBUG("unary '%s'\n", yytext);
+       expr_arg_begin = lf3_arg_index;
+       expr_arg_end = lf3_arg_index + 2;
+       expr_arg_type = TOKEN_TEST;
+       BEGIN ARG;
+}
+
+-(fprint|fprint0|printf)\0 {
+       LF3_DEBUG("unary '%s'\n", yytext);
+       expr_arg_begin = lf3_arg_index;
+       expr_arg_end = lf3_arg_index + 2;
+       expr_arg_type = TOKEN_ACTION;
+       BEGIN ARG;
+}
+
+-(fprintf)\0 {
+       LF3_DEBUG("binary '%s'\n", yytext);
+       expr_arg_begin = lf3_arg_index;
+       expr_arg_end = lf3_arg_index + 3;
+       expr_arg_type = TOKEN_ACTION;
+       BEGIN ARG;
+}
+
+<ARG>[^\0]*\0 {
+       LF3_DEBUG("arg '%s'\n", yytext);
+       if (lf3_arg_index + 1 == expr_arg_end) {
+               yylval.u_range[0] = expr_arg_begin;
+               yylval.u_range[1] = expr_arg_end;
+               BEGIN INITIAL;
+               return expr_arg_type;
+       } else {
+               /* Continue in ARG state. */
+       }
+}
+
+<ARG><<EOF>> {
+       LF3_FATAL_AT(expr_arg_begin, "missing argument to '%s'\n", lf3_arg[expr_arg_begin]);
+}
+
+-exec\0 {
+       LF3_DEBUG("begin-exec '%s'\n", yytext);
+       expr_arg_begin = lf3_arg_index;
+       BEGIN EXEC_ARG;
+}
+
+<EXEC_ARG>(\+|\;)\0 {
+       LF3_DEBUG("end-exec '%s'\n", yytext);
+       yylval.u_range[0] = expr_arg_begin;
+       yylval.u_range[1] = lf3_arg_index + 1;
+       BEGIN INITIAL;
+       return TOKEN_ACTION;
+}
+
+<EXEC_ARG>[^\0]*\0 {
+       LF3_DEBUG("exec-arg '%s'\n", yytext);
+       /* Continue in EXEC_ARG state/ */
+}
+
+<EXEC_ARG><<EOF>> {
+       LF3_FATAL_AT(expr_arg_begin, "missing argument to '%s'\n", lf3_arg[expr_arg_begin]);
+}
+
+\(\0           return TOKEN_LEFT_PAREN;
+\)\0           return TOKEN_RIGHT_PAREN;
+(\!|-not)\0    return TOKEN_NOT;
+-(a|and)\0     return TOKEN_AND;
+-(o|or)\0      return TOKEN_OR;
+,\0            return TOKEN_COMMA;
+
+(.|\n)*\0 {
+       LF3_FATAL_AT(lf3_arg_index, "unknown test '%s'\n", lf3_arg[lf3_arg_index]);
+}
+
+%%
+
+static YY_BUFFER_STATE lf3_buffer_state;
+
+/* Set lf3_buffer_state to the next argument in lf3_arg. */
+int yywrap(void)
+{
+       const char *arg;
+
+       LF3_DEBUG_D(lf3_arg_count);
+       LF3_DEBUG_D(lf3_arg_index);
+
+       lf3_arg_index++;
+       if (lf3_arg_index >= lf3_arg_count)
+               return 1; /* Reached end of arguments. */
+
+       yy_flush_buffer(lf3_buffer_state);
+       yy_delete_buffer(lf3_buffer_state);
+       arg = lf3_arg[lf3_arg_index];
+       LF3_DEBUG_S(arg);
+       lf3_buffer_state = yy_scan_bytes(arg, strlen(arg) + 1);
+       yy_switch_to_buffer(lf3_buffer_state);
+
+       return 0;
+}
+
+int lf3_lexer_init(const char **arg, int arg_index, int arg_count)
+{
+       /* arg: a non-empty array of arguments (usually argv of main())
+        * arg_index: the index of the start of the the
+        * expression. For example:
+        *    { "-true" }
+        * or
+        *    { "-inum", "0", "-print-fid" }.
+        * arg_count: the total number of arguments (usually argc of main())
+        *
+        * We do it this way instead of a 0-based array containing
+        * just the expression arguments so that error messages can provide
+        * argument indexes that agree with the user provided command line. */
+
+       LF3_DEBUG_S(arg[arg_index]);
+       LF3_DEBUG_D(arg_count);
+       LF3_DEBUG_D(arg_index);
+
+       assert(0 <= arg_index);
+       assert(arg_index < arg_count);
+
+       lf3_arg = arg;
+       lf3_arg_count = arg_count;
+       lf3_arg_index = arg_index - 1;
+
+       /* The first call to yywrap() increments lf3_arg_index and
+        * initializes the lexer buffer lf3_arg[lf3_arg_index]. This
+        * is why we use arg_index -1 above. */
+       yywrap();
+
+       return 0;
+}
diff --git a/lipe/src/lipe_find3/lf3_parse.y b/lipe/src/lipe_find3/lf3_parse.y
new file mode 100644 (file)
index 0000000..ab3c77f
--- /dev/null
@@ -0,0 +1,1322 @@
+%{
+#include <assert.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <grp.h>
+#include <limits.h>
+#include <printf.h>
+#include <pwd.h>
+#include <signal.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include "lipe_version.h"
+#include "list.h"
+#include "lf3_debug.h"
+#include "lf3_lexer.h"
+#include "lf3_parse_format.h"
+#include "lf3_printf.h"
+#include "list.h"
+#include "xmalloc.h"
+
+static void yyerror(const char *str);
+
+/* Like /usr/bin/getopt we pretend to be another program when we print
+ * error messages. */
+
+int lf3_debug;
+const char *lf3_progname_;
+static unsigned long long lf3_now = -1;
+
+#ifndef LF3_SCAN_PROGRAM
+# define LF3_SCAN_PROGRAM "lipe_scan3"
+#endif
+
+static const char LF3_SCAN_COMMAND[] = LF3_SCAN_PROGRAM " --script=/dev/stdin";
+static const char LF3_SCM_SCAN_PROC[] = "lipe-scan";
+static char *lf3_policy_body;
+static bool lf3_policy_body_has_action;
+
+typedef unsigned long long __ull;
+
+static const char lf3_gensym_prefix[] = "%lf3";
+static unsigned int lf3_gensym_count;
+
+struct lf3_file {
+       struct lipe_list_head lf_link;
+       const char *lf_path;
+       const char *lf_port;
+       const char *lf_mutex;
+};
+
+static LIPE_LIST_HEAD(lf3_file_list); /* lf3_file */
+static LIPE_LIST_HEAD(lf3_genvar_list); /* name -> initializer expr */
+static LIPE_LIST_HEAD(lf3_init_list); /* expr, NULL, to be evaluated in before thunk of dynamic-wind */
+static LIPE_LIST_HEAD(lf3_fini_list); /* expr, NULL, ... after ...  */
+
+static void lf3_list_add_tail(struct lipe_list_head *list, const char *car, const char *cdr)
+{
+       struct lf3_pair *lfp;
+
+       lfp = xcalloc(1, sizeof(*lfp));
+       if (car != NULL)
+               lfp->lfp_car = xstrdup(car);
+
+       if (cdr != NULL)
+               lfp->lfp_cdr = xstrdup(cdr);
+
+       lipe_list_add_tail(&lfp->lfp_link, list);
+}
+
+/* Generate a fresh unique symbol with the given prefix and name. Name
+ * is only for readability of intermediate code. */
+static const char *lf3_gensym(const char *prefix,
+                            const char *func,
+                            const char *name)
+{
+#if 1
+       return xsprintf("%s:%s:%u", prefix, name, lf3_gensym_count++);
+#else /* Include function name in symbol (too verbose). */
+       return xsprintf("%s:%s:%s:%u", prefix, func, name, lf3_gensym_count++);
+#endif
+}
+
+static const char *lf3_genvar(const char *prefix,
+                            const char *func,
+                            const char *name,
+                            const char *expr)
+{
+       const char *sym = lf3_gensym(prefix, func, name);
+
+       lf3_list_add_tail(&lf3_genvar_list, sym, expr);
+
+       return sym;
+}
+
+#define LF3_GENSYM(name) \
+       lf3_gensym(lf3_gensym_prefix, __FUNCTION__, name)
+
+#define LF3_GENVAR(name, expr) \
+       lf3_genvar(lf3_gensym_prefix, __FUNCTION__, name, expr)
+
+#define LF3_GENVARF(name, fmt, args...) \
+       lf3_genvar(lf3_gensym_prefix, __FUNCTION__, name, xsprintf(fmt, ##args))
+
+static void lf3_init_add(const char *expr)
+{
+       lf3_list_add_tail(&lf3_init_list, expr, NULL);
+}
+
+static void lf3_fini_add(const char *expr)
+{
+       lf3_list_add_tail(&lf3_fini_list, expr, NULL);
+}
+
+#define LF3_FINI_ADDF(fmt, args...) \
+       lf3_fini_add(xsprintf(fmt, ##args))
+
+struct lf3_unit {
+       const char *apu_name;
+       __ull apu_value;
+};
+
+static int lf3_parse_unit(const struct lf3_unit *apu, const char *name, __ull *value)
+{
+       int i;
+
+       for (i = 0; apu[i].apu_name != NULL; i++) {
+               if (strcmp(apu[i].apu_name, name) == 0) {
+                       *value = apu[i].apu_value;
+                       return 0;
+               }
+       }
+
+       return -EINVAL;
+}
+
+/* Parse a [+-]VAL[UNIT] (for example a "+10M" argument or '-size')
+ * string into components. */
+static int lf3_parse_numeric(const struct lf3_unit *apu, const char *str, char *cmp, __ull *val, __ull *unit)
+{
+       int saved_errno;
+       char *unit_name = NULL;
+       int rc;
+
+       switch (str[0]) {
+       case '+':
+               *cmp = '>';
+               break;
+       case '-':
+               *cmp = '<';
+               break;
+       default:
+               *cmp = '=';
+               break;
+       }
+
+       if (*cmp != '=')
+               str++;
+
+       if (!isdigit(str[0]))
+               return -EINVAL;
+
+       saved_errno = errno;
+       errno = 0;
+       *val = strtoul(str, &unit_name, 0);
+       if (errno != 0)
+               return -errno;
+
+       errno = saved_errno;
+
+       rc = lf3_parse_unit(apu, unit_name, unit);
+       if (rc < 0)
+               return rc;
+
+       return 0;
+}
+
+static const struct lf3_unit lf3_size_units[] = {
+       { "", 512 }, /* XXX This is the default used by find. */
+       { "b", 512 },
+       { "c", 1 },
+       { "w", 2 },
+       { "k", 1024 },
+       { "M", 1048576 },
+       { "G", 1073741824 },
+       { NULL, 0 },
+};
+
+static int lf3_parse_user(const char *name, id_t *id)
+{
+       struct passwd pw;
+       struct passwd *rpw = NULL;
+       char *buf = NULL;
+       size_t buf_size;
+       char *end;
+       int rc;
+
+       end = NULL;
+       errno = 0;
+       *id = strtoul(name, &end, 10);
+       if (errno == 0 && *name != '\0' && *end == '\0') {
+               rc = 0;
+               goto out;
+       }
+
+       buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
+       if (buf_size == -1)
+               buf_size = 16384;
+
+       buf = xcalloc(1, buf_size);
+
+       rc = getpwnam_r(name, &pw, buf, buf_size, &rpw);
+       if (rpw == NULL) {
+               if (rc != 0)
+                       rc = -rc;
+               else
+                       rc = INT_MIN;
+
+               goto out;
+       }
+
+       *id = pw.pw_uid;
+       rc = 0;
+out:
+       free(buf);
+
+       return rc;
+}
+
+static int lf3_parse_group(const char *name, id_t *id)
+{
+       struct group gr;
+       struct group *rgr = NULL;
+       char *buf = NULL;
+       size_t buf_size;
+       char *end;
+       int rc;
+
+       end = NULL;
+       errno = 0;
+       *id = strtoul(name, &end, 10);
+       if (errno == 0 && *name != '\0' && *end == '\0') {
+               rc = 0;
+               goto out;
+       }
+
+       buf_size = sysconf(_SC_GETGR_R_SIZE_MAX);
+       if (buf_size == -1)
+               buf_size = 16384;
+
+       buf = xcalloc(1, buf_size);
+
+       rc = getgrnam_r(name, &gr, buf, buf_size, &rgr);
+       if (rgr == NULL) {
+               if (rc != 0)
+                       rc = -rc;
+               else
+                       rc = INT_MIN;
+
+               goto out;
+       }
+
+       *id = gr.gr_gid;
+       rc = 0;
+out:
+       free(buf);
+
+       return rc;
+}
+
+static char *lf3_user_expr(int begin, int end)
+{
+       const char *name = lf3_arg[begin + 1];
+       id_t id;
+       int rc;
+
+       assert(begin + 2 == end);
+
+       rc = lf3_parse_user(name, &id);
+       if (rc == INT_MIN)
+               LF3_FATAL_AT(begin + 1, "'%s' is not the name of a known user\n", name);
+       else if (rc < 0)
+               LF3_FATAL_AT(begin + 1, "cannot get UID for user '%s': %s\n", name, strerror(-rc));
+
+       return xsprintf("(= (uid) %llu)", (__ull)id);
+}
+
+static char *lf3_group_expr(int begin, int end)
+{
+       const char *name = lf3_arg[begin + 1];
+       id_t id;
+       int rc;
+
+       assert(begin + 2 == end);
+
+       rc = lf3_parse_group(name, &id);
+       if (rc == INT_MIN)
+               LF3_FATAL_AT(begin + 1, "'%s' is not the name of a known group\n", name);
+       else if (rc < 0)
+               LF3_FATAL_AT(begin + 1, "cannot get GID for group '%s': %s\n", name, strerror(-rc));
+
+       return xsprintf("(= (gid) %llu)", (__ull)id);
+}
+
+static inline bool is_power_of_2(__ull x)
+{
+       return x != 0 && (x & (x - 1)) == 0;
+}
+
+static char *lf3_size_expr(int begin, int end)
+{
+       const char *size_str = lf3_arg[begin + 1];
+       char cmp;
+       __ull val;
+       __ull unit;
+       int rc;
+
+       /* -size n[cwbkMG]
+        * File uses n units of space, rounding up.  The following suffixes can be used: */
+
+       assert(begin + 2 == end);
+
+       rc = lf3_parse_numeric(lf3_size_units, size_str, &cmp, &val, &unit);
+       if (rc < 0)
+               LF3_FATAL_AT(begin + 1, "invalid size '%s'\n", size_str);
+
+       /* From find(1): The + and - prefixes signify greater than and
+        * less than, as usual; i.e., an exact size of n units does
+        * not match. Bear in mind that the size is rounded up to the
+        * next unit. Therefore -size -1M is not equivalent to -size
+        * -1048576c. The former only matches empty files, the latter
+        * matches files from 0 to 1,048,575 bytes. */
+
+       if ((cmp == '=' && val == 0) || unit == 1)
+               return xsprintf("(%c (size) %llu)", cmp, val);
+       else if (is_power_of_2(unit))
+               return xsprintf("(%c (round-up-power-of-2 (size) %llu) %llu)",
+                               cmp,
+                               unit,
+                               unit * val);
+       else
+               return xsprintf("(%c (round-up (size) %llu) %llu)",
+                               cmp,
+                               unit,
+                               unit * val);
+}
+
+static const struct lf3_unit lf3_time_units[] = {
+       { "", 0 }, /* See below */
+       { "s", 1 },
+       { "m", 60 },
+       { "h", 3600 },
+       { "d", 86400 },
+       { NULL, 0 },
+};
+
+static char *lf3_time_expr(char which, __ull unit_sec_def, int begin, int end)
+{
+       const char *time_str = lf3_arg[begin + 1];
+       char cmp;
+       __ull val;
+       __ull unit_sec;
+       int rc;
+
+       assert(begin + 2 == end);
+       assert(which == 'a' || which == 'm' || which == 'c');
+
+       rc = lf3_parse_numeric(lf3_time_units, time_str, &cmp, &val, &unit_sec);
+       if (rc < 0)
+               LF3_FATAL_AT(begin + 1, "invalid time '%s'\n", time_str);
+
+       /* -atime n
+        *
+        * File was last accessed n*24 hours ago. When find figures
+        * out how many 24-hour periods ago the file was last
+        * accessed, any fractional part is ignored, so to match
+        * -atime +1, a file has to have been accessed at least two
+        * days ago. */
+
+       if (unit_sec == 0)
+               unit_sec = unit_sec_def;
+
+       /* (cmp (quotient (- now (atime)) unit_sec) val) */
+
+       return xsprintf("(%c" /* cmp */
+                       " (quotient (- %llu" /* now */
+                       " (%ctime))" /* which */
+                       " %llu)" /* unit_sec */
+                       " %llu)" /* val */,
+                       cmp, lf3_now, which, unit_sec, val);
+}
+
+static const struct lf3_unit lf3_other_units[] = {
+       { "", 1 },
+       { NULL, 0 },
+};
+
+/* Numeric tests for uid, gid, inum, links... */
+static char *lf3_numeric_expr(const char *thunk, int begin, int end)
+{
+       const char *first = lf3_arg[begin] + 1; /* Drop leading '-'. */
+       const char *arg_str = lf3_arg[begin + 1];
+       char cmp;
+       __ull val;
+       __ull unit;
+       int rc;
+
+       assert(begin + 2 == end);
+
+       rc = lf3_parse_numeric(lf3_other_units, arg_str, &cmp, &val, &unit);
+       if (rc < 0)
+               LF3_FATAL_AT(begin + 1, "invalid argument to %s '%s'\n", first, arg_str);
+
+       assert(unit == 1);
+
+       return xsprintf("(%c (%s) %llu)", cmp, thunk, val);
+}
+
+static char *lf3_type_expr(int begin, int end)
+{
+       const char *type_str = lf3_arg[begin + 1];
+       const char *s;
+       // bool found_one_type = false;
+       char *expr = NULL;
+       size_t expr_size = 0;
+       FILE *expr_stream = NULL;
+
+       assert(begin + 2 == end);
+
+       expr_stream = open_memstream(&expr, &expr_size);
+       fprintf(expr_stream, "(or");
+
+       for (s = type_str; *s != '\0'; s++) {
+               mode_t fmt;
+
+               switch (*s) {
+               case ',':
+                       continue;
+               case 'b':
+                       fmt = S_IFBLK;
+                       break;
+               case 'c':
+                       fmt = S_IFCHR;
+                       break;
+               case 'd':
+                       fmt = S_IFDIR;
+                       break;
+               case 'p':
+                       fmt = S_IFIFO;
+                       break;
+               case 'f':
+                       fmt = S_IFREG;
+                       break;
+               case 'l':
+                       fmt = S_IFLNK;
+                       break;
+               case 's':
+                       fmt = S_IFSOCK;
+                       break;
+               default:
+                       LF3_FATAL_AT(begin + 1, "invalid type '%s'\n", type_str);
+               }
+
+               fprintf(expr_stream, " (= (logand (mode) %llu) %llu)", (__ull)S_IFMT, (__ull)fmt);
+       }
+
+       fprintf(expr_stream, ")");
+       fclose(expr_stream);
+
+       return expr;
+}
+
+static const char LF3_FNMATCH[] = "fnmatch?";
+static const char LF3_FNMATCH_CI[] = "fnmatch-ci?";
+static const char LF3_STRING_EQ[] = "string=?";
+static const char LF3_STRING_CI_EQ[] = "string-ci=?";
+
+static char *lf3_fnmatch_expr(const char *which /* LF3_FNMATCH, LF3_FNMATCH_CI */,
+                            const char *pattern,
+                            const char *caller /* call-with-name, ...-path */)
+{
+       const char *p_str;
+       const char *v_match;
+
+       /* From fnmatch(3): the fnmatch() function checks whether the
+        * string argument matches the pattern argument, which is a
+        * shell wildcard pattern (see glob(7)).
+        *
+        * From glob(7): A string is a wildcard pattern if it contains
+        * one of the characters '?', '*' or '['.  Globbing is the
+        * operation that expands a wildcard pattern into the list of
+        * pathnames matching the pattern.
+        */
+       if (strpbrk(pattern, "?*[") == NULL) {
+               /* pattern is a plain string. Do strength reduction. */
+               if (strcmp(which, LF3_FNMATCH) == 0)
+                       which = LF3_STRING_EQ;
+               else if (strcmp(which, LF3_FNMATCH_CI) == 0)
+                       which = LF3_STRING_CI_EQ;
+       }
+
+       /* We want something equivalent to the following but without
+        * creating a closure for each inode:
+        * (let ((match?
+        *        (lambda (str)
+        *          (which pattern str))))
+        *   (any match? (thunk)))
+        */
+
+       p_str = LF3_GENSYM("str");
+       v_match = LF3_GENVARF("match",
+                            "(lambda (%s) (%s %Q %s))",
+                            p_str, which, pattern, p_str);
+
+       return xsprintf("(%s %s)", caller, v_match);
+}
+
+static char *lf3_name_expr(int begin, int end)
+{
+       /* FIXME find(1) warns if the pattern passed to -name includes
+        * a slash. We should do that here too. */
+
+       assert(begin + 2 == end);
+
+       return lf3_fnmatch_expr(LF3_FNMATCH, lf3_arg[begin + 1], "call-with-name");
+}
+
+static char *lf3_iname_expr(int begin, int end)
+{
+       assert(begin + 2 == end);
+
+       return lf3_fnmatch_expr(LF3_FNMATCH_CI, lf3_arg[begin + 1], "call-with-name");
+}
+
+static char *lf3_path_expr(int begin, int end)
+{
+       assert(begin + 2 == end);
+
+       return lf3_fnmatch_expr(LF3_FNMATCH, lf3_arg[begin + 1], "call-with-relative-path");
+}
+
+static char *lf3_ipath_expr(int begin, int end)
+{
+       assert(begin + 2 == end);
+
+       return lf3_fnmatch_expr(LF3_FNMATCH_CI, lf3_arg[begin + 1], "call-with-relative-path");
+}
+
+static char *lf3_exec_plus_expr(int begin, int end)
+{
+       char *expr = NULL;
+       size_t expr_size = 0;
+       FILE *stream = NULL;
+       const char *v_pipe;
+       const char *v_mutex;
+       const char *v_printer;
+       const char *cmd;
+       int i;
+
+       assert(strcmp(lf3_arg[end - 1], "+") == 0); /* Thanks to scanner. */
+
+       /*
+        * $ find . -type f -exec echo {} bar +;
+        * find: missing argument to `-exec'
+        *
+        * $ find . -type f -exec echo foo{} +;
+        * find: In ‘-exec ... {} +’ the ‘{}’ must appear by itself, but you specified ‘foo{}’
+        *
+        * $ find . -type f -exec echo foo {} {} +;
+        * find: Only one instance of {} is supported with -exec ... +
+        */
+       if (strcmp(lf3_arg[end - 2], "{}") != 0)
+               LF3_FATAL_AT(begin, "missing argument to exec\n");
+
+       cmd = lf3_arg[begin + 1];
+       LF3_DEBUG_S(cmd);
+
+       for (i = begin + 2; i < end - 2; i++)
+               if (strstr(lf3_arg[i], "{}") != NULL)
+                       LF3_FATAL_AT(i, "only one instance of {} is supported with -exec ... +\n");
+
+       /* (define v_pipe (make-exec-plus-pipe cmd arg[2] ... arg[end - 3]) */
+       stream = open_memstream(&expr, &expr_size);
+       fprintf(stream, "(make-exec-plus-pipe");
+
+       /* lf3_arg[begin + 0] == "-exec"
+        * lf3_arg[begin + 1] == cmd
+        * ...
+        * lf3_arg[end   - 2] == "{}"
+        * lf3_arg[end   - 1] == "+" */
+       for (i = begin + 1; i < end - 2; i++)
+               lf3_fprintf(stream, " %Q", lf3_arg[i]);
+
+       fprintf(stream, ")");
+       fclose(stream);
+
+       v_pipe = LF3_GENVAR("pipe", expr);
+       v_mutex = LF3_GENVAR("mutex", "(make-mutex)");
+       v_printer = LF3_GENVARF("print", "(make-printer %s %s #\\x00)",
+                               v_pipe, v_mutex);
+
+       LF3_FINI_ADDF("(close-exec-plus-pipe %s)", v_pipe);
+
+       return xsprintf("(call-with-absolute-path %s)", v_printer);
+}
+
+/* Make an exec arg expression from a single find -exec ... \; argument.
+ *
+ * When evaluated, the file name will be in the variable @sym. From
+ * find(1): The string `{}' is replaced by the current file name being
+ * processed everywhere it occurs in the arguments to the command, not
+ * just in arguments where it is alone, as in some versions of find.
+ *
+ * "{}" => sym
+ * "chacha" => "chacha"
+ * "{}cha" => (string-append sym "cha")
+ * "cha {} cha" => (string-append "cha " sym " cha")
+ */
+static int lf3_exec_arg(FILE *stream, int index, const char *arg, const char *sym)
+{
+       if (strchr(arg, '{') == NULL)
+               return lf3_fprintf(stream, " %Q", arg);
+
+       if (strcmp(arg, "{}") == 0)
+               return fprintf(stream, " %s", sym);
+
+       fprintf(stream, " (string-append");
+
+       while (*arg != '\0') {
+               const char *lbr = strchr(arg, '{');
+
+               if (lbr == NULL) {
+                       lf3_fprintf(stream, " %Q", arg);
+                       break;
+               }
+
+               if (lbr[1] != '}')
+                       LF3_FATAL_AT(index, "missing '}' in argument to exec\n");
+
+               if (lbr == arg)
+                       fprintf(stream, " %s", sym);
+               else
+                       lf3_fprintf(stream, " %.*Q %s", (int)(lbr - arg), arg, sym);
+
+               arg = lbr + 2;
+       }
+
+       fprintf(stream, ")");
+
+       return 0;
+}
+
+static char *lf3_exec_expr(int begin, int end)
+{
+       char *expr = NULL;
+       size_t expr_size = 0;
+       FILE *stream = NULL;
+       const char *p_path;
+       const char *v_exec;
+       const char *cmd;
+       const char *last;
+       int i;
+
+       /* "-exec \;", end - begin == 2, no command */
+       if (end - begin < 3)
+               LF3_FATAL_AT(begin, "missing argument to exec\n");
+
+       cmd = lf3_arg[begin + 1];
+       last = lf3_arg[end - 1];
+       LF3_DEBUG_S(cmd);
+       LF3_DEBUG_S(last);
+       LF3_DEBUG_D(begin);
+       LF3_DEBUG_D(end);
+       LF3_DEBUG_D(end - begin);
+
+       if (strcmp(last, "+") == 0)
+               return lf3_exec_plus_expr(begin, end);
+
+       assert(strcmp(last, ";") == 0); /* Thanks to the scanner. */
+
+       p_path = LF3_GENSYM("path");
+
+       stream = open_memstream(&expr, &expr_size);
+       lf3_fprintf(stream, "(lambda (%s) (= 0 (system* %Q", p_path, cmd);
+
+       for (i = begin + 2; i < end - 1; i++) {
+               const char *arg = lf3_arg[i];
+
+               LF3_DEBUG_S(arg);
+               lf3_exec_arg(stream, i, arg, p_path);
+       }
+
+       fprintf(stream, ")))");
+       fclose(stream);
+
+       v_exec = LF3_GENVAR("exec", expr);
+
+       return xsprintf("(call-with-absolute-path %s)", v_exec);
+}
+
+/* This is a fallback handler used for -print, ... */
+static char *lf3_call_expr(int begin, int end)
+{
+       const char *first = lf3_arg[begin] + 1; /* Drop leading '-'. */
+       char *expr = NULL;
+       size_t expr_size = 0;
+       FILE *expr_stream = NULL;
+       int i;
+
+       /* first is a scheme procedure name, the rest are strings */
+
+       expr_stream = open_memstream(&expr, &expr_size);
+       fprintf(expr_stream, "(%s", first);
+
+       for (i = begin + 1; i < end; i++)
+               lf3_fprintf(expr_stream, " %Q", lf3_arg[i]);
+
+       fprintf(expr_stream, ")");
+       fclose(expr_stream);
+
+       return expr;
+}
+
+static struct lf3_file *lf3_make_file(const char *path)
+{
+       struct lf3_file *lf;
+
+       /* -fprint file
+        *
+        * True; print the full file name into file file.  If file
+        * does not exist when find is run, it is created; if it does
+        * exist, it is truncated. The file names `/dev/stdout' and
+        * `/dev/stderr' are handled specially; they refer to the
+        * standard output and standard error output,
+        * respectively. The output file is always created, even if
+        * the predicate is never matched. See the UNUSUAL FILENAMES
+        * section for information about how unusual characters in
+        * filenames are handled.
+        *
+        * Passing "/dev/stdout" to open below would cause
+        * truncation. Which is not what is intended or what find
+        * does.
+        *
+        * To preserve the find semantics (and do what the user
+        * expects) each file (-fprint or -fprintf target) should be
+        * opened only once and then shared among the relevant action
+        * handlers. */
+
+       lipe_list_for_each_entry(lf, &lf3_file_list, lf_link)
+               if (strcmp(path, lf->lf_path) == 0)
+                       return lf;
+
+       lf = xcalloc(1, sizeof(*lf));
+
+       lf->lf_path = xstrdup(path);
+
+       if (strcmp(path, "/dev/stdout") == 0) {
+               lf->lf_port = LF3_GENVARF("port", "(current-output-port)");
+       } else if (strcmp(path, "/dev/stderr") == 0) {
+               lf->lf_port = LF3_GENVARF("port", "(current-error-port)");
+       } else {
+               lf->lf_port = LF3_GENVARF("port", "(open-file %Q %Q)", path, "w");
+               LF3_FINI_ADDF("(close-port %s)", lf->lf_port);
+       }
+
+       lf->lf_mutex = LF3_GENVARF("mutex", "(make-mutex)");
+
+       return lf;
+}
+
+static const char *lf3_make_printer(const char *path, int delim)
+{
+       struct lf3_file *lf;
+       const char *v_printer;
+
+       lf = lf3_make_file(path);
+
+       if (delim < 0)
+               v_printer = LF3_GENVARF("print", "(make-printer %s %s #f)",
+                                       lf->lf_port, lf->lf_mutex);
+       else
+               v_printer = LF3_GENVARF("print", "(make-printer %s %s #\\x%02hhx)",
+                                       lf->lf_port, lf->lf_mutex, (unsigned char)delim);
+
+       return v_printer;
+}
+
+static char *lf3_print_expr2(const char *file, int delim)
+{
+       const char *v_printer = lf3_make_printer(file, delim);
+
+       /* FIXME How to ensure that client mount path was supplied? */
+
+       return xsprintf("(call-with-relative-path %s)", v_printer);
+}
+
+/* -print */
+static char *lf3_print_expr(int begin, int end)
+{
+       return lf3_print_expr2("/dev/stdout", '\n');
+}
+
+/* -print0 */
+static char *lf3_print0_expr(int begin, int end)
+{
+       return lf3_print_expr2("/dev/stdout", '\0');
+}
+
+/* -fprint file */
+static char *lf3_fprint_expr(int begin, int end)
+{
+       const char *file = lf3_arg[begin + 1];
+
+       assert(begin + 2 == end);
+
+       return lf3_print_expr2(file, '\n');
+}
+
+/* -fprint0 file */
+static char *lf3_fprint0_expr(int begin, int end)
+{
+       const char *file = lf3_arg[begin + 1];
+
+       assert(begin + 2 == end);
+
+       return lf3_print_expr2(file, '\0');
+}
+
+static char *lf3_fprintf_expr2(const char *file, int fmt_index, const char *fmt)
+{
+       const char *v_printer = lf3_make_printer(file, -1);
+       char *fmt_expr = NULL;
+       size_t fmt_expr_size = 0;
+       char *arg_expr = NULL;
+       size_t arg_expr_size = 0;
+       FILE *fmt_stream;
+       FILE *arg_stream;
+
+       fmt_stream = open_memstream(&fmt_expr, &fmt_expr_size);
+       arg_stream = open_memstream(&arg_expr, &arg_expr_size);
+       lf3_parse_format(fmt_index, fmt, fmt_stream, arg_stream);
+       fclose(fmt_stream);
+       fclose(arg_stream);
+
+       return xsprintf("(%s (format #f %s %s))", v_printer, fmt_expr, arg_expr);
+}
+
+/* -printf fmt */
+static char *lf3_printf_expr(int begin, int end)
+{
+       const char *fmt = lf3_arg[begin + 1];
+
+       assert(begin + 2 == end);
+
+       return lf3_fprintf_expr2("/dev/stdout", begin + 1, fmt);
+}
+
+/* -fprintf file fmt */
+static char *lf3_fprintf_expr(int begin, int end)
+{
+       const char *file = lf3_arg[begin + 1];
+       const char *fmt = lf3_arg[begin + 2];
+
+       assert(begin + 3 == end);
+
+       return lf3_fprintf_expr2(file, begin + 2, fmt);
+}
+
+static char *lf3_delete_expr(int begin, int end)
+{
+       const char *v_deleter;
+
+       v_deleter = LF3_GENVARF("delete", "(make-deleter)");
+
+       LF3_FINI_ADDF("(close-deleter %s)", v_deleter);
+
+       return xsprintf("(%s (file-fid))", v_deleter);
+}
+
+/* Build a simple expression, begin and end are indexes into lf3_arg.
+ * lf3_arg[begin] is the test or action name ('-size', '-print',
+ * '-delete'). Expression arguments start at lf3_arg[begin + 1] and end
+ * at lf3_arg[end] (non inclusive). */
+static char *lf3_simple_expr(int begin, int end)
+{
+       const char *first = lf3_arg[begin] + 1; /* Test or action name with leading '-' removed. */
+
+       /* The scanner has already checked the argument count for us. */
+
+#define X2(name, expr) \
+       do {                                    \
+               if (strcmp(first, name) == 0)   \
+                       return (expr);          \
+       } while (0)
+
+#define X1(name) X2(#name, lf3_ ## name ## _expr(begin, end))
+
+       X1(user);
+       X1(group);
+       X1(name);
+       X1(iname);
+       X1(path);
+       X1(ipath);
+       X1(size);
+       X1(type);
+       X1(exec);
+       // X1(perm); TODO
+       // X1(used); TODO
+       X1(print);
+       X1(print0);
+       X1(fprint);
+       X1(fprint0);
+       X1(printf);
+       X1(fprintf);
+       X1(delete);
+
+       /* lipe_scan attribute names are consistent with struct stat member names. */
+       X2("inum",  lf3_numeric_expr("ino",   begin, end)); // find uses "-inum", lipe_scan uses "ino".
+       X2("uid",   lf3_numeric_expr("uid",   begin, end)); // ...
+       X2("gid",   lf3_numeric_expr("gid",   begin, end));
+       X2("projid", lf3_numeric_expr("projid",   begin, end));
+       X2("links", lf3_numeric_expr("nlink", begin, end));
+
+       X2("amin",  lf3_time_expr('a',    60, begin, end));
+       X2("atime", lf3_time_expr('a', 86400, begin, end));
+       X2("cmin",  lf3_time_expr('c',    60, begin, end));
+       X2("ctime", lf3_time_expr('c', 86400, begin, end));
+       X2("mmin",  lf3_time_expr('m',    60, begin, end));
+       X2("mtime", lf3_time_expr('m', 86400, begin, end));
+
+       X2("true",  xstrdup("#t"));
+       X2("false", xstrdup("#f"));
+       X2("wholename", lf3_path_expr(begin, end));
+       X2("iwholename", lf3_ipath_expr(begin, end));
+
+       X2("quit", "(lipe-scan-break 0)");
+
+#undef X1
+#undef X2
+
+       return lf3_call_expr(begin, end);
+}
+
+static char *lf3_unary_expr(const char *op, const char *e1)
+{
+       return xsprintf("(%s %s)", op, e1);
+}
+
+static char *lf3_binary_expr(const char *op, const char *e1, const char *e2)
+{
+       return xsprintf("(%s %s %s)", op, e1, e2);
+}
+
+%}
+
+/* Bison declarations. */
+
+%define parse.error custom
+%define parse.lac full
+%locations
+
+%union
+{
+       int u_range[2];
+       char *u_expr;
+}
+
+%left TOKEN_COMMA /* Lowest precedence */
+%left TOKEN_OR
+%left TOKEN_AND
+%token TOKEN_NOT /* Highest precedence */
+%token TOKEN_LEFT_PAREN
+%token TOKEN_RIGHT_PAREN
+%token <u_range> TOKEN_ACTION
+%token <u_range> TOKEN_TEST
+
+/* We use three levels of expr to handle shift reduce conflicts around
+ * 'not' and implicit 'and'. */
+%type <u_expr> expr
+%type <u_expr> expr1
+%type <u_expr> expr2
+
+%% /* rules */
+
+prog   : expr {
+       lf3_policy_body = $1;
+}
+
+expr   : expr TOKEN_AND expr { $$ = lf3_binary_expr("and", $1, $3); }
+       | expr TOKEN_OR expr { $$ = lf3_binary_expr("or", $1, $3); }
+       | expr TOKEN_COMMA expr { $$ = lf3_binary_expr("begin", $1, $3); }
+       | expr1 { $$ = $1; }
+       ;
+
+expr1  : expr1 expr2 { $$ = lf3_binary_expr("and", $1, $2); } /* Implicit 'and'. */
+       | expr2 { $$ = $1; }
+       ;
+
+expr2  : TOKEN_LEFT_PAREN expr TOKEN_RIGHT_PAREN { $$ = $2; }
+       | TOKEN_NOT expr2 { $$ = lf3_unary_expr("not", $2); }
+       | TOKEN_ACTION { $$ = lf3_simple_expr($1[0], $1[1]); lf3_policy_body_has_action = true; }
+       | TOKEN_TEST { $$ = lf3_simple_expr($1[0], $1[1]); }
+       ;
+
+%% /* end rules, start user code */
+
+static void yyerror(const char *str)
+{
+       LF3_FATAL_AT(lf3_arg_index, "internal error: %s\n", str);
+}
+
+static const char *lf3_symbol_name(enum yysymbol_kind_t kind)
+{
+       switch (kind) {
+       case YYSYMBOL_YYEOF: return "end of arguments";
+       case YYSYMBOL_TOKEN_COMMA: return "','";
+       case YYSYMBOL_TOKEN_OR: return "'-or'";
+       case YYSYMBOL_TOKEN_AND: return "'-and'";
+       case YYSYMBOL_TOKEN_NOT: return "'-not'";
+       case YYSYMBOL_TOKEN_LEFT_PAREN: return "'('";
+       case YYSYMBOL_TOKEN_RIGHT_PAREN: return "')'";
+       case YYSYMBOL_TOKEN_ACTION: return "action";
+       case YYSYMBOL_TOKEN_TEST: return "test";
+       default: return yysymbol_name(kind);
+       }
+}
+
+static int yyreport_syntax_error(const yypcontext_t *ctx)
+{
+       enum { TOKENMAX = 16 };
+       yysymbol_kind_t expected[TOKENMAX];
+       yysymbol_kind_t lookahead;
+       int i, n;
+       char *msg = NULL;
+       size_t msg_size = 0;
+       FILE *msg_stream = NULL;
+       int rc = 0;
+
+       n = yypcontext_expected_tokens(ctx, expected, TOKENMAX);
+       if (n < 0) {
+               /* Forward errors to yyparse. */
+               rc = n;
+               return n;
+       }
+
+       assert(n != 0);
+
+       msg_stream = open_memstream(&msg, &msg_size);
+
+       /* Report the tokens expected at this point. */
+       if (n == 1)
+               fprintf(msg_stream, "expected %s", lf3_symbol_name(expected[0]));
+       else
+               fprintf(msg_stream, "expected one of %s", lf3_symbol_name(expected[0]));
+
+       for (i = 1; i < n - 1; ++i)
+               fprintf(msg_stream, ", %s", lf3_symbol_name(expected[i]));
+
+       /* Report the unexpected token. */
+       lookahead = yypcontext_token(ctx);
+       if (lookahead != YYSYMBOL_YYEMPTY)
+               fprintf(msg_stream, " before %s", lf3_symbol_name(lookahead));
+
+       fclose(msg_stream);
+
+       /* We store the current argument index in first_line. See YY_USER_ACTION. */
+       LF3_FATAL_AT(yypcontext_location(ctx)->first_line, "%s\n", msg);
+
+       return rc;
+}
+
+enum {
+       LF3_OPT_DEBUG,
+       LF3_OPT_DEBUG_SCAN,
+       LF3_OPT_NOW,
+       LF3_OPT_SCAN_COMMAND,
+       LF3_OPT_THREAD_COUNT,
+       LF3_OPT_HELP = 'h',
+       LF3_OPT_VERSION = 'v',
+};
+
+static void help(void)
+{
+       printf(
+"Usage: %s [OPTION]... DEVICE [EXPRESSION]\n"
+"Find files on a Lustre target device\n"
+"\n"
+"Mandatory arguments to long options are mandatory for short options too.\n"
+"  --debug                     enable debugging\n"
+"  --debug-scan                enable scan debugging\n"
+/* --now=EPOCH                 use EPOCH instead of current time (for testing) */
+/* --scan-command=COMMAND      use 'cat' to print the code we would send to lipe_scan3 */
+"  --thread-count=COUNT        use COUNT scaninng threads\n"
+"  -h, --help                  display this help text and exit\n"
+"  -v, --version               output version information and exit\n",
+       program_invocation_short_name);
+}
+
+static const struct option options[] = {
+       { "debug", no_argument, 0, LF3_OPT_DEBUG },
+       { "debug-scan", no_argument, 0, LF3_OPT_DEBUG_SCAN },
+       { "now", required_argument, 0, LF3_OPT_NOW },
+       { "scan-command", required_argument, 0, LF3_OPT_SCAN_COMMAND },
+       { "thread-count", required_argument, 0, LF3_OPT_THREAD_COUNT },
+       { "help", no_argument, 0,  LF3_OPT_HELP },
+       { "version", no_argument, 0, LF3_OPT_VERSION },
+       { NULL },
+};
+
+int main(int argc, char *argv[])
+{
+       const char *scan_command = LF3_SCAN_COMMAND;
+       const char *thread_count = NULL;
+       bool debug_scan = false;
+       int rc;
+       int c;
+
+       lipe_version_init();
+
+       rc = register_printf_specifier('K', &lf3_printf_extension_K, &lf3_printf_extension_K_arginfo);
+       assert(rc == 0);
+       rc = register_printf_specifier('P', &lf3_printf_extension_P, &lf3_printf_extension_P_arginfo);
+       assert(rc == 0);
+       rc = register_printf_specifier('Q', &lf3_printf_extension_Q, &lf3_printf_extension_Q_arginfo);
+       assert(rc == 0);
+
+       while ((c = getopt_long(argc, argv, "+hv", options, NULL)) != -1) {
+               switch (c) {
+               case LF3_OPT_DEBUG:
+                       lf3_debug = 1;
+                       break;
+               case LF3_OPT_DEBUG_SCAN:
+                       debug_scan = true;
+                       break;
+               case LF3_OPT_NOW:
+                       lf3_now = strtoull(optarg, NULL, 0);
+                       break;
+               case LF3_OPT_SCAN_COMMAND:
+                       scan_command = optarg;
+                       break;
+               case LF3_OPT_THREAD_COUNT:
+                       thread_count = optarg;
+                       break;
+               case LF3_OPT_HELP:
+                       help();
+                       exit(EXIT_SUCCESS);
+               case LF3_OPT_VERSION:
+                       lipe_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       fprintf(stderr, "Try '%s --help for more information.\n",
+                               program_invocation_short_name);
+                       exit(EXIT_FAILURE + 1);
+               }
+       }
+
+       LF3_DEBUG_D(argc);
+       LF3_DEBUG_D(optind);
+       LF3_DEBUG_D(debug_scan);
+       LF3_DEBUG_S(scan_command);
+       LF3_DEBUG_S(PACKAGE_VERSION);
+       LF3_DEBUG_S(LIPE_REVISION);
+
+       if (debug_scan)
+               lf3_init_add("(lipe-debug-enable #t)");
+
+       if (lf3_now == -1)
+               lf3_now = time(NULL);
+
+       LF3_DEBUG_U(lf3_now);
+
+       if (thread_count != NULL) {
+               char *end = NULL;
+               long val;
+
+               errno = 0;
+               val = strtol(thread_count, &end, 0);
+               if (errno != 0 || *end != '\0' || val < 0)
+                       LF3_FATAL("invalid thread count '%s'\n", thread_count);
+       } else {
+               thread_count = "(lipe-getopt-thread-count)";
+       }
+
+       LF3_DEBUG_S(thread_count);
+
+       if (optind == argc) {
+               fprintf(stderr,
+                       "Usage: %s [OPTION]... DEVICE [EXPRESSION]\n"
+                       "Try '%s --help for more information.\n",
+                       program_invocation_short_name, program_invocation_short_name);
+               exit(EXIT_FAILURE + 1);
+       }
+
+       /* TODO Handle multiple devices. */
+
+       const char *device_path = argv[optind];
+       LF3_DEBUG_S(device_path);
+
+       if (optind + 1 == argc) {
+               /* If no expression is supplied then use '-true'. */
+               static const char *lf3_true = "-true";
+               lf3_lexer_init(&lf3_true, 0, 1);
+       } else {
+               lf3_lexer_init((const char **)argv, optind + 1, argc);
+       }
+
+       rc = yyparse();
+
+       LF3_DEBUG_D(rc);
+       LF3_DEBUG_S(lf3_policy_body);
+       LF3_DEBUG_B(lf3_policy_body_has_action);
+
+       if (rc != 0 || lf3_policy_body == NULL)
+               LF3_FATAL("internal argument parsing error\n");
+
+       /* At the end of expression parsing we have built the following:
+        *
+        * 1. lf3_genvar_list: a list of pairs of genvars and their initial values.
+        *
+        * 2. lf3_init_list: a list of initial actions (for example opening output files or creating -exec pipe-lines).
+        *
+        * 3. lf3_policy_body: the expression we evaluate for each
+        *    inode, for example '(or (= (uid) 500) (= (gid) 507))'.
+        *    This may or may not contain an action (it does iff lf3_policy_body_has_action).
+        *
+        * 4. lf3_fini_list: a list of final actions (closing pipes or ports).
+        *
+        * From this we build:
+        *
+        * 1. policy_thunk: a thunk containing the policy_body
+        *    with an implicit action added if lf3_policy_body does
+        *    not contain one. For example
+        *     '(lambda () (and (or (= (uid) 500) (= (gid) 507)) (print-file-fid)))'.
+        *
+        * 2. scm_code:
+        *
+        *      (let* (lf3_genvar_list)
+        *        (dynamic-wind
+        *          (lambda () lf3_init_list)
+        *          (lambda () (lipe-scan3 device_path ... policy_thunk ...))
+        *          (lambda () lf3_fini_list)))
+        *
+        * Scheme does not like empty lambda bodies, so to make this
+        * work we need to fixup lf3_init_list and lf3_fini_list.
+        */
+       if (lipe_list_empty(&lf3_init_list))
+               lf3_init_add("#t");
+
+       if (lipe_list_empty(&lf3_fini_list))
+               lf3_fini_add("#t");
+
+       /* find(1) adds an implicit -print action if no action was
+        * specified in the expression. We use -print-file-fid as the
+        * implicit action. */
+       char *policy_thunk = lf3_policy_body_has_action ?
+               xsprintf("(lambda () %s)", lf3_policy_body) :
+               xsprintf("(lambda () (and %s (print-file-fid)))", lf3_policy_body);
+       LF3_DEBUG_S(policy_thunk);
+
+       char *scm_code = xsprintf(
+               "(use-modules (lipe) (lipe find))\n"
+               "\n"
+               "(let* (%P)\n"
+               "  (dynamic-wind\n"
+               "    (lambda () %K)\n"
+               "    (lambda () (%s\n"
+               "        %Q\n"
+               "        %s\n"
+               "        %s\n"
+               "        %s\n"
+               "        %s))\n"
+               "    (lambda () %K)))\n",
+               &lf3_genvar_list,
+               &lf3_init_list,
+               LF3_SCM_SCAN_PROC, /* lipe-scan */
+                       device_path,
+                       "(lipe-getopt-client-mount-path)",
+                       policy_thunk,
+                       "(lipe-getopt-required-attrs)",
+                       thread_count,
+               &lf3_fini_list);
+       LF3_DEBUG_S(scm_code);
+
+       errno = 0;
+       FILE *scan_pipe = popen(scan_command, "w");
+       if (scan_pipe == NULL) {
+               if (errno == 0)
+                       errno = ENOMEM; /* See popen(3). */
+               LF3_FATAL("cannot execute scan command '%s' for '%s': %s\n",
+                         scan_command, device_path, strerror(errno));
+       }
+
+       rc = fputs(scm_code, scan_pipe);
+       if (rc < 0)
+               LF3_FATAL("cannot write scan command to '%s' for '%s': %s\n",
+                         scan_command, device_path, strerror(errno));
+       rc = pclose(scan_pipe);
+       LF3_DEBUG_D(rc);
+       if (rc < 0)
+               LF3_FATAL("error on close of pipe to '%s' for '%s': %s\n",
+                         scan_command, device_path, strerror(errno));
+
+       if (WIFEXITED(rc)) {
+               exit(WEXITSTATUS(rc));
+       } else if (WIFSIGNALED(rc)) {
+               int sig = WTERMSIG(rc);
+
+               raise(sig);
+
+               LF3_FATAL("scan of '%s' terminated by signal %d (%s)\n",
+                         device_path, sig, strsignal(sig));
+       } else {
+               LF3_FATAL("scan of '%s' terminated with unexpected status %d\n", device_path, rc);
+       }
+}
diff --git a/lipe/src/lipe_find3/lf3_parse_format.c b/lipe/src/lipe_find3/lf3_parse_format.c
new file mode 100644 (file)
index 0000000..f74016e
--- /dev/null
@@ -0,0 +1,456 @@
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <malloc.h>
+#include <ctype.h>
+#include "lf3_debug.h"
+#include "lf3_parse_format.h"
+#include "lf3_printf.h"
+#include "xmalloc.h"
+
+static bool strstartswith(const char *str, const char *prefix)
+{
+       return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+static int isodigit(int c)
+{
+       return isdigit(c) && c != '9';
+}
+
+static int isnzdigit(int c)
+{
+       return isdigit(c) && c != '0';
+}
+
+static int lf3_parse_printf_width(const char *fmt, int *width)
+{
+       char *end = NULL;
+       long value = 0;
+
+       if (!isnzdigit(*fmt)) {
+               *width = 0;
+               return 0;
+       }
+
+       errno = 0;
+       value = strtol(fmt, &end, 0);
+       if (errno != 0)
+               return -errno;
+
+       if (value != (int)value)
+               return -EINVAL;
+
+       *width = value;
+
+       return end - fmt;
+}
+
+static int lf3_parse_printf_precision(const char *fmt, int *precision)
+{
+       char *end = NULL;
+       long value = 0;
+
+       /* From printf(3): An optional precision, in the form of a
+        * period ('.') followed by an optional decimal digit
+        * string. ... If the precision is given as just '.', the
+        * precision is taken to be zero. A negative precision is
+        * taken as if the precision were omitted. */
+
+       if (*fmt != '.') {
+               *precision = 0;
+               return 0;
+       }
+
+       errno = 0;
+       value = strtol(fmt + 1, &end, 0);
+       if (errno != 0)
+               return -errno;
+
+       if (value != (int)value)
+               return -EINVAL;
+
+       if (value < 0)
+               *precision = 0;
+       else
+               *precision = value;
+
+       return end - fmt;
+}
+
+/* LF3_FORMAT_AT() but with a char offset. */
+#define LF3_FATAL_AT_OFFSET(index, offset, fmt, args...)       \
+       LF3_FATAL_AT(index, "offset %d: "fmt, offset, ##args)
+
+/* Parse a single format directive ("%%", "%a", "%A@", ...) from the
+ * start of fmt. Return the length of the directive. */
+static int lf3_parse_format_directive(int index, const char *fmt, int n, FILE *s_fmt, FILE *s_arg)
+{
+       bool alt_form = false;
+       bool zero_pad = false;
+       bool left_justify = false;
+       const char *rbr;
+       char *ext = NULL;
+       int width = 0;
+       int precision = 0;
+       int n0 = n;
+       int rc;
+       int c;
+       int k;
+
+       LF3_DEBUG_D(n);
+       LF3_DEBUG_S(&fmt[n]);
+
+       assert(fmt[n] == '%');
+       n++;
+
+       if (fmt[n] == '%') {
+               fputc('%', s_fmt);
+               n++;
+               return n;
+       }
+
+       /* Flags. */
+       for (; fmt[n] != '\0'; n++) {
+               switch (fmt[n]) {
+               case '#':
+                       alt_form = true;
+                       break;
+               case '0':
+                       zero_pad = true;
+                       break;
+               case '-':
+                       left_justify = true;
+                       break;
+               default:
+                       goto width;
+               }
+       }
+
+width:
+       rc = lf3_parse_printf_width(&fmt[n], &width);
+       if (rc < 0)
+               LF3_FATAL_AT_OFFSET(index, n, "invalid width in format directive '%s'\n", fmt);
+
+       n += rc;
+
+       rc = lf3_parse_printf_precision(&fmt[n], &precision);
+       if (rc < 0)
+               LF3_FATAL_AT_OFFSET(index, n, "invalid precision in format directive '%s'\n", fmt);
+
+       n += rc;
+
+       // TODO Support flags, width, precision.
+       // (format #t "~5,'*d" 12)   -| ***12
+       // (format #t "~5,'0d" 12)   -| 00012
+       // (format #t "~3d"    1234) -| 1234
+
+       if (alt_form || zero_pad || left_justify)
+               LF3_FATAL_AT_OFFSET(index, n0, "invalid format string '%s', flags are not supported\n", fmt);
+
+       if (width != 0)
+               LF3_FATAL_AT_OFFSET(index, n0, "invalid format string '%s', width is not supported\n", fmt);
+
+       if (precision != 0)
+               LF3_FATAL_AT_OFFSET(index, n0, "invalid format string '%s', precision is not supported\n", fmt);
+
+#define LF3_EMIT_S(c, expr, args...)                           \
+       do {                                                    \
+               fprintf(s_fmt, "~%c", c);                       \
+               lf3_fprintf(s_arg, " "expr, ##args);            \
+       } while (0)
+
+#define LF3_EMIT_N(c, expr, args...)                           \
+       do {                                                    \
+               fprintf(s_fmt, "~%c", c);                       \
+               lf3_fprintf(s_arg, " "expr, ##args);            \
+       } while (0)
+
+#define LF3_EMIT_U(c) \
+       LF3_FATAL_AT_OFFSET(index, n, "unsupported format specifier '%c'\n", c)
+
+       /* Specifier. */
+       c = fmt[n++];
+       switch (c) {
+       case '\0':
+               LF3_FATAL_AT_OFFSET(index, n0, "incomplete format directive '%s'\n", fmt);
+       case 'a': /* fall through */
+       case 'c': /* fall through */
+       case 't': /* t == mtime */
+               /* [acm]time in ctime() format */
+               if (c == 't')
+                       c = 'm';
+               LF3_EMIT_S('a', "(strftime %Q (localtime (%ctime)))", "%a %b %e %T %Y", c);
+               break;
+       case 'A': /* fall through */
+       case 'C': /* fall through */
+       case 'T': /* T == mtime */
+               /* %[ACM] time in k format (k is @ or a strftime directive) */
+               c = tolower(c);
+               if (c == 't')
+                       c = 'm';
+               k = fmt[n++];
+               if (k == '\0')
+                       LF3_FATAL_AT_OFFSET(index, n0, "incomplete format directive '%s'\n", fmt);
+               else if (k == '@')
+                       LF3_EMIT_N('d', "(%ctime)", c);
+               else if (isalpha(k))
+                       LF3_EMIT_S('a', "(strftime \"%%%c\" (localtime (%ctime)))", k, c);
+               else
+                       LF3_FATAL_AT_OFFSET(index, n, "unrecognized strftime format specifier '%c' in '%s'\n", k, fmt);
+
+               break;
+       case 'b':
+               LF3_EMIT_N('d', "(blocks)");
+               break;
+       case 'd': /* File's depth in the directory tree; 0 means the file is a starting-point. */
+               LF3_EMIT_U(c);
+               break;
+       case 'D': /* The device number on which the file exists (st_dev in decimal). */
+               LF3_EMIT_U(c);
+               break;
+       case 'f': /* File's name with any leading directories removed (only the last element) */
+               LF3_EMIT_S('a', "(name)");
+               break;
+       case 'F': /* Type of the filesystem the file is on; this value can be used for -fstype. */
+               LF3_EMIT_U(c);
+               break;
+       case 'g': /* File's group name, or numeric group ID if group has no name. */
+               LF3_EMIT_S('a', "(group)");
+               break;
+       case 'G': /* File's numeric group ID. */
+               LF3_EMIT_N('d', "(gid)");
+               break;
+       case 'h':
+               /* Leading directories of file's name (all but the last
+                * element). If the file name contains no slashes (since it is
+                * in the current directory) the %h specifier expands to '.'. */
+               LF3_EMIT_S('a', "(call-with-relative-path dirname)");
+               break;
+       case 'H': /* Starting point under which file was found. */
+               LF3_EMIT_S('a', "(lipe-scan-client-mount-path)");
+               break;
+       case 'i': /* File's inode number (in decimal). */
+               LF3_EMIT_N('d', "(ino)");
+               break;
+       case 'k': /* disk space used in 1KB blocks (rounded up). */
+               LF3_EMIT_N('d', "(quotient (+ (blocks) 1) 2)");
+               break;
+       case 'l': /* symlink target or empty string if not symlink */
+               LF3_EMIT_U(c);
+               break;
+       case 'm': /* File's permission bits (in octal). */
+               LF3_EMIT_N('o', "(logand (mode) #o07777)"); /* FIXME octal. */
+               break;
+       case 'M': /* Files permissions in symbolic form. */
+               LF3_EMIT_U(c);
+               break;
+       case 'n': /* Number of hard links to file. */
+               LF3_EMIT_N('d', "(nlink)");
+               break;
+       case 'p': /* File's name with 'starting-point'. */
+               LF3_EMIT_S('a', "(absolute-path)");
+               break;
+       case 'P': /* File's name without 'starting-point' */
+               LF3_EMIT_S('a', "(relative-path)");
+               break;
+       case 's': /* File's size in bytes. */
+               LF3_EMIT_N('d', "(size)");
+               break;
+       case 'S': /* File's sparseness. */
+               LF3_EMIT_N('f', "(/ (* 512 (blocks)) (size))");
+               break;
+       case 'u': /* File's user name, or numeric user ID if the user has no name. */
+               LF3_EMIT_S('a', "(user)");
+               break;
+       case 'U': /* File's numeric user ID. */
+               LF3_EMIT_N('d', "(uid)");
+               break;
+       case 'y': /* single char type */
+               LF3_EMIT_S('a', "(type->char (type))");
+               break;
+       case 'Y': /* File's  type (like %y), plus follow symlinks: 'L'=loop, 'N'=nonexistent, '?' ... */
+               LF3_EMIT_U(c);
+               break;
+       case 'Z': /* (SELinux only) file's security context. */
+               LF3_EMIT_U(c);
+               break;
+       case '{':
+               rbr = strchr(&fmt[n], '}');
+               if (rbr == NULL)
+                       LF3_FATAL_AT_OFFSET(index, n0, "missing '}' in format directive '%s'\n", fmt);
+
+               ext = xstrndup(&fmt[n], rbr - &fmt[n]);
+               LF3_DEBUG_S(ext);
+
+               // TODO
+               // client-mount-path
+               // device-name
+               // device-path
+               // fsname
+               // parent-fid
+               // parent-fids
+               // pools
+               if (strcmp(ext, "fid") == 0)
+                       LF3_EMIT_S('a', "(fid)");
+               else if (strcmp(ext, "projid") == 0 ||
+                        strcmp(ext, "stripe-count") == 0 ||
+                        strcmp(ext, "mirror-count") == 0)
+                       LF3_EMIT_N('d', "(%s)", ext);
+               else if (strstartswith(ext, "xattr:"))
+                       LF3_EMIT_S('a', "(xattr-ref-string %Q)", ext + strlen("xattr:"));
+               else
+                       LF3_FATAL_AT_OFFSET(index, n, "unrecognized format directive '%s'\n", ext);
+
+               n += rbr - &fmt[n] + 1; /* Step past '}'. */
+               break;
+       default:
+               LF3_FATAL_AT_OFFSET(index, n - 1, "unrecognized format specifier '%c' in '%s'\n", c, fmt);
+               break;
+       }
+
+       free(ext);
+
+       return n;
+}
+
+/* Write single char c properly escaped for inclusion in a scheme
+ * (format ...) format string. format uses '~' in the way that printf
+ * uses '%' so we need to tilde-escape '~'. */
+static int lf3_fprintc_scm_format(FILE *stream, int c)
+{
+       if (c == '~')
+               return fprintf(stream, "~~");
+       else
+               return lf3_fprintc_scm_string(stream, c);
+}
+
+/* Parse a "single char" from a find format string @fmt satrting at
+ * offset n which is not a part of a '%' directive. */
+static int lf3_parse_format_char(int index, const char *fmt, int n, int *p)
+{
+       int c;
+       int x;
+
+       assert(fmt[n] != '\0' && fmt[n] != '%');
+
+       *p = fmt[n++];
+       if (*p != '\\')
+               return n;
+
+       switch ((c = fmt[n++])) {
+       default:
+               /* This is what find does. */
+               *p = c;
+               return n;
+       case '\0':
+               LF3_FATAL_AT_OFFSET(index, n, "stray '\\' at end of format directive\n");
+       case 'a':
+               *p = '\a';
+               return n;
+       case 'b':
+               *p = '\b';
+               return n;
+       case 't':
+               *p = '\t';
+               return n;
+       case 'n':
+               *p = '\n';
+               return n;
+       case 'v':
+               *p = '\v';
+               return n;
+       case 'f':
+               *p = '\f';
+               return n;
+       case 'r':
+               *p = '\r';
+               return n;
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+               x = c - '0';
+
+               if (isodigit(fmt[n]))
+                       x = (x << 3) | (fmt[n++] - '0');
+
+               if (isodigit(fmt[n]))
+                       x = (x << 3) | (fmt[n++] - '0');
+
+               *p = (unsigned char)x;
+
+               return n;
+       }
+}
+
+/* Parse the find format string to a scheme format call.
+ * @index is the index of fmt in the original argv and is only for error messages.
+*/
+int lf3_parse_format(int index, const char *fmt, FILE *s_fmt, FILE *s_arg)
+{
+       int n = 0;
+
+       fprintf(s_fmt, "\"");
+
+       while (fmt[n] != '\0') {
+               const char *mod = strchrnul(&fmt[n], '%');
+
+               /* Copy chars upto next '%' from fmt to s_fmt. */
+               while (&fmt[n] < mod) {
+                       int c;
+
+                       n = lf3_parse_format_char(index, fmt, n, &c);
+                       assert(&fmt[n] <= mod);
+                       lf3_fprintc_scm_format(s_fmt, c);
+               }
+
+               if (fmt[n] == '\0')
+                       break;
+
+               assert(fmt[n] == '%');
+               n = lf3_parse_format_directive(index, fmt, n, s_fmt, s_arg);
+       }
+
+       fprintf(s_fmt, "\"");
+
+       return n;
+}
+
+#if 0
+int lf3_debug = 1;
+const char *lf3_progname_;
+
+int main(int agrc, char *argv[])
+{
+       const char *fmt = argv[1];
+       char *fmt_expr = NULL;
+       size_t fmt_expr_size = 0;
+       char *arg_expr = NULL;
+       size_t arg_expr_size = 0;
+       FILE *s_fmt;
+       FILE *s_arg;
+
+       register_printf_specifier('Q', &lf3_printf_extension_q, &lf3_printf_extension_q_arginfo);
+
+       s_fmt = open_memstream(&fmt_expr, &fmt_expr_size);
+       s_arg = open_memstream(&arg_expr, &arg_expr_size);
+       lf3_parse_format(0, fmt, s_fmt, s_arg);
+       fclose(s_fmt);
+       fclose(s_arg);
+
+       LF3_DEBUG_S(fmt_expr);
+       LF3_DEBUG_S(arg_expr);
+
+       printf("(format #t %s%s)\n", fmt_expr, arg_expr);
+
+       return 0;
+}
+#endif
diff --git a/lipe/src/lipe_find3/lf3_parse_format.h b/lipe/src/lipe_find3/lf3_parse_format.h
new file mode 100644 (file)
index 0000000..1685815
--- /dev/null
@@ -0,0 +1,7 @@
+#ifndef _LF3_PARSE_FORMAT_H_
+#define _LF3_PARSE_FORMAT_H_
+#include <stdio.h>
+
+int lf3_parse_format(int index, const char *fmt, FILE *s_fmt, FILE *s_arg);
+
+#endif
diff --git a/lipe/src/lipe_find3/lf3_printf.c b/lipe/src/lipe_find3/lf3_printf.c
new file mode 100644 (file)
index 0000000..5b95e97
--- /dev/null
@@ -0,0 +1,129 @@
+#include <ctype.h>
+#include <stdio.h>
+#include <printf.h>
+#include <limits.h> /* INT_MAX */
+#include "lf3_printf.h"
+
+/* Write single char c properly escaped for inclusion in a double
+ * quoted scheme string. */
+int lf3_fprintc_scm_string(FILE *stream, int c)
+{
+       switch (c) {
+       case '\a':
+               return fprintf(stream, "\\a");
+       case '\b':
+               return fprintf(stream, "\\b");
+       case '\t':
+               return fprintf(stream, "\\t");
+       case '\n':
+               return fprintf(stream, "\\n");
+       case '\v':
+               return fprintf(stream, "\\v");
+       case '\f':
+               return fprintf(stream, "\\f");
+       case '\r':
+               return fprintf(stream, "\\r");
+       case '"':
+               return fprintf(stream, "\\\"");
+       case '\\':
+               return fprintf(stream, "\\\\");
+       default:
+               if (isprint(c) && ' ' <= c && c < 0177) /* 0177 == DEL */
+                       return fprintf(stream, "%c", c);
+               else
+                       return fprintf(stream, "\\x%02x", c);
+       }
+}
+
+/* printf extension (%K) to print a list of forms. */
+int lf3_printf_extension_K(FILE *stream,
+                          const struct printf_info *info,
+                          const void *const *args)
+{
+       struct lipe_list_head *lis;
+       struct lf3_pair *lfp;
+       int i = 0, n = 0;
+
+       lis = *((struct lipe_list_head **)(args[0]));
+       lipe_list_for_each_entry(lfp, lis, lfp_link) {
+               n += fprintf(stream, "%s%s", i == 0 ? "" : " ", lfp->lfp_car);
+               i++;
+       }
+
+       return n;
+}
+
+int lf3_printf_extension_K_arginfo(const struct printf_info *info, size_t n,
+                                 int *argtypes, int *sizes)
+{
+       if (n > 0) {
+               argtypes[0] = PA_POINTER;
+               sizes[0] = sizeof(void *);
+       }
+
+       return 1;
+}
+
+
+/* printf extension (%P) to print a list of pairs */
+int lf3_printf_extension_P(FILE *stream,
+                         const struct printf_info *info,
+                         const void *const *args)
+{
+       struct lipe_list_head *lis;
+       struct lf3_pair *lfp;
+       int i = 0, n = 0;
+
+       lis = *((struct lipe_list_head **)(args[0]));
+       lipe_list_for_each_entry(lfp, lis, lfp_link) {
+               n += fprintf(stream, "%s(%s %s)", i == 0 ? "" : " ",
+                            lfp->lfp_car, lfp->lfp_cdr);
+               i++;
+       }
+
+       return n;
+}
+
+int lf3_printf_extension_P_arginfo(const struct printf_info *info, size_t n,
+                                 int *argtypes, int *sizes)
+{
+       if (n > 0) {
+               argtypes[0] = PA_POINTER;
+               sizes[0] = sizeof(void *);
+       }
+
+       return 1;
+}
+
+/* printf extension (%Q) to print a double quoted slash-escaped string */
+int lf3_printf_extension_Q(FILE *stream,
+                         const struct printf_info *info,
+                         const void *const *args)
+{
+       const char *s;
+       int prec = info->prec < 0 ? INT_MAX : info->prec;
+       int n = 0;
+       int i;
+
+       s = *((const char **)(args[0]));
+
+       n += fprintf(stream, "\"");
+
+       for (i = 0; i < prec && s[i] != '\0'; i++)
+               n += lf3_fprintc_scm_string(stream, s[i]);
+
+       n += fprintf(stream, "\"");
+
+       return n;
+}
+
+int lf3_printf_extension_Q_arginfo(const struct printf_info *info, size_t n,
+                                 int *argtypes, int *sizes)
+{
+       if (n > 0) {
+               argtypes[0] = PA_POINTER;
+               sizes[0] = sizeof(const char *);
+       }
+
+       return 1;
+}
diff --git a/lipe/src/lipe_find3/lf3_printf.h b/lipe/src/lipe_find3/lf3_printf.h
new file mode 100644 (file)
index 0000000..fda15bd
--- /dev/null
@@ -0,0 +1,71 @@
+#ifndef _LF3_PRINTF_H_
+#define _LF3_PRINTF_H_
+#include "lf3_debug.h"
+#include <stdio.h>
+#include <printf.h>
+#include <stdarg.h>
+#include "list.h"
+
+struct lf3_pair {
+       struct lipe_list_head lfp_link;
+       const char *lfp_car;
+       const char *lfp_cdr;
+};
+
+/* We don't want to use -Wno-format, but the -Wformat checker does not
+ * recognize our fundamental human right to define printf
+ * extensions. So use lf3_fprintf() which has no
+ * __attribute__((__format__((__printf__((__tueday__, __thursday__)))))) annotiation
+ * to hide out from gcc.  */
+static inline int lf3_fprintf(FILE *file, const char *fmt, ...)
+{
+       va_list ap;
+       int size;
+
+       va_start(ap, fmt);
+       size = vfprintf(file, fmt, ap);
+       va_end(ap);
+
+       return size;
+}
+
+static inline char *xsprintf(const char *fmt, ...)
+{
+       char *str;
+       va_list args;
+       int rc;
+
+       va_start(args, fmt);
+       rc = vasprintf(&str, fmt, args);
+       va_end(args);
+
+       if (rc < 0)
+               LF3_FATAL("out of memory\n");
+
+       return str;
+}
+
+int lf3_fprintc_scm_string(FILE *stream, int c);
+
+int lf3_printf_extension_K(FILE *stream,
+                          const struct printf_info *info,
+                          const void *const *args);
+
+int lf3_printf_extension_K_arginfo(const struct printf_info *info, size_t n,
+                                  int *argtypes, int *sizes);
+
+int lf3_printf_extension_P(FILE *stream,
+                          const struct printf_info *info,
+                          const void *const *args);
+
+int lf3_printf_extension_P_arginfo(const struct printf_info *info, size_t n,
+                                  int *argtypes, int *sizes);
+
+int lf3_printf_extension_Q(FILE *stream,
+                          const struct printf_info *info,
+                          const void *const *args);
+
+int lf3_printf_extension_Q_arginfo(const struct printf_info *info, size_t n,
+                                  int *argtypes, int *sizes);
+
+#endif
diff --git a/lipe/src/lipe_find3/lipe/find.scm b/lipe/src/lipe_find3/lipe/find.scm
new file mode 100644 (file)
index 0000000..9fb29f7
--- /dev/null
@@ -0,0 +1,192 @@
+(define-module (lipe find)
+  #:use-module (lipe)
+  #:use-module (ice-9 format)
+  #:use-module (ice-9 iconv) ; bytevector->string
+  #:use-module (ice-9 popen) ;; open-pipe*
+  #:use-module (ice-9 threads) ;; make-mutex, with-mutex
+  #:use-module (rnrs base) ;; assert
+  #:use-module (rnrs bytevectors)
+  #:export (
+           absolute-path
+           relative-path
+           call-with-absolute-path
+           call-with-relative-path
+           call-with-name
+           name
+           names
+           fnmatch-ci?
+           round-up
+           round-up-power-of-2
+           make-exec-plus-pipe
+           close-exec-plus-pipe
+           make-printer
+           make-deleter
+           close-deleter
+           type
+           type->char
+           char->type
+           user
+           group
+           xattr-ref-string
+           ))
+
+(define-inlinable (any1 proc lis)
+  ;; Single list version of (any ...) from (srfi srfi-1)
+  (let loop ((lis lis))
+    (if (pair? lis)
+       (or (proc (car lis))
+           (loop (cdr lis)))
+       #f)))
+
+(define-inlinable (any-string proc lis)
+  (or (any1 proc lis) ""))
+
+(define (absolute-path)
+  ;; For use in -printf. Returns "" if file has no paths.
+  (any-string identity (absolute-paths)))
+
+(define (call-with-absolute-path proc)
+  (any1 proc (absolute-paths)))
+
+(define (relative-path)
+  ;; For use in -printf. Returns "" if file has no paths.
+  (any-string identity (relative-paths)))
+
+(define (call-with-relative-path proc)
+  (any1 proc (relative-paths)))
+
+(define (name)
+  ;; For use in -printf. Returns "" if file has no name.
+  (any-string cdr (links)))
+
+(define (names)
+  (map cdr (links)))
+
+(define (call-with-name proc)
+  (any1 proc (names)))
+
+(define-inlinable (fnmatch-ci? pattern string)
+  (fnmatch? pattern string FNM_CASEFOLD))
+
+(define-inlinable (round-up-power-of-2 x m)
+  (1+ (logior (- x 1) (- m 1))))
+
+(define-inlinable (round-up x m)
+  (* (quotient (+ x m -1) m) m))
+
+(define (make-exec-plus-pipe . args)
+  (apply open-pipe* OPEN_WRITE "xargs" "--no-run-if-empty" "--null" "--" args))
+
+(define (close-exec-plus-pipe pipe)
+  (close-pipe pipe))
+
+(define (make-printer port mutex delim)
+  (assert (port? port))
+  (assert (mutex? mutex))
+  (assert (or (char? delim) (eq? delim #f)))
+  (if (char? delim)
+      (lambda (str)
+       (with-mutex mutex
+                   (display str port)
+                   (display delim port)))
+      (lambda (str)
+       (with-mutex mutex
+                   (display str port)))))
+
+(define (make-deleter-pipe)
+  (let ((client-mount-path (lipe-scan-client-mount-path)))
+    (assert (string? client-mount-path)) ;; FIXME stat
+    (assert (not (string=? client-mount-path ""))) ;; FIXME stat
+    (open-pipe* OPEN_WRITE
+               "xargs"
+               "--max-args=1024"
+               "--no-run-if-empty"
+               "--null"
+               "--"
+               "lfs"
+               "rmfid"
+               client-mount-path)))
+
+(define (make-deleter)
+  (let ((mutex (make-mutex))
+       (pipe #f))
+    (lambda (fid)
+      ;; (assert (or (fid? fid) (eq? fid #f)))
+      (with-mutex mutex
+       (cond (fid
+              ;; Defer opening pipe until we need it. We don't know
+              ;; the client-mount-path here and we are too lazy to
+              ;; find it.
+              (if (not pipe)
+                  (set! pipe (make-deleter-pipe)))
+              (display fid pipe)
+              (display #\x00 pipe))
+             (else
+              (if pipe
+                  (close-pipe pipe))
+              (set! pipe #f)))))))
+
+(define (close-deleter deleter)
+  (deleter #f))
+
+(define S_IFMT   #o0170000)
+(define S_IFDIR  #o0040000)
+(define S_IFCHR  #o0020000)
+(define S_IFBLK  #o0060000)
+(define S_IFREG  #o0100000)
+(define S_IFIFO  #o0010000)
+(define S_IFLNK  #o0120000)
+(define S_IFSOCK #o0140000)
+
+(define (type)
+  (logand (mode) S_IFMT))
+
+(define (perm)
+  (logand (mode) #o07777))
+
+(define (char->type c)
+  (cond
+   ((eq? c #\d) S_IFDIR)
+   ((eq? c #\c) S_IFCHR)
+   ((eq? c #\b) S_IFBLK)
+   ((eq? c #\f) S_IFREG)
+   ((eq? c #\p) S_IFIFO)
+   ((eq? c #\l) S_IFLNK)
+   ((eq? c #\s) S_IFSOCK)))
+
+(define (type->char t)
+  (cond
+   ((= t S_IFDIR) #\d)
+   ((= t S_IFCHR) #\c)
+   ((= t S_IFBLK) #\b)
+   ((= t S_IFREG) #\f)
+   ((= t S_IFIFO) #\p)
+   ((= t S_IFLNK) #\l)
+   ((= t S_IFSOCK) #\s)))
+
+(define %lipe-getpw-mutex (make-mutex))
+
+(define (%lipe-uid->user id)
+  (catch 'misc-error
+        (lambda ()
+          (passwd:name (with-mutex %lipe-getpw-mutex (getpw id))))
+        (lambda (key . args)
+          (number->string id))))
+
+(define %lipe-getgr-mutex (make-mutex))
+
+(define (%lipe-gid->group id)
+  (catch 'misc-error
+        (lambda ()
+          (group:name (with-mutex %lipe-getgr-mutex (getgr id))))
+        (lambda (key . args)
+          (number->string id))))
+
+(define (user)
+  (%lipe-uid->user (uid)))
+
+(define (group)
+  (%lipe-gid->group (gid)))
+
+(define (xattr-ref-string name)
+  (utf8->string (or (xattr-ref name) #vu8())))
diff --git a/lipe/src/lipe_find3/lipe_scan3_mock.scm b/lipe/src/lipe_find3/lipe_scan3_mock.scm
new file mode 100755 (executable)
index 0000000..fdab34b
--- /dev/null
@@ -0,0 +1,227 @@
+#!/usr/bin/guile -s
+!#
+;; XXX XXX XXX This is out of sync with lipe_find3 and lipe_scan3.
+;;
+;; This is a mock testing utility for find-to-scheme expression
+;; conversion.
+;;
+;; HOWTO
+;;
+;; Use env (/usr/bin/env) to run this script with the attribute values
+;; you want in the environment. To avoid possible conflicts naming
+;; conflicts attributes are prefixed with a '%'
+;; (MOCK-SYMBOL-ENV-PREFIX) character. For example
+;;
+;;  $ env %fid='"0x200000404:0x1:0x0"' %ino=7 ./lipe_find3 --scan-command=./lipe_scan3_mock.scm -- /dev/mapper/mds1_flakey -inum 7 -print-fid
+;;
+;; In the mock environment, we use strings to represent FIDs so the
+;; value should be in two layers of quotes (single then double). For example
+;;
+;;  $ env %fid='"0x200000404:0x1:0x0"' ...
+;;
+;; If this script is used as intended then it should not fail (exit
+;; with non zero status). It should either print or not print the file
+;; according to what lipe_find* would do for a file with specified
+;; attributes.
+;;
+;; EXAMPLES
+;;
+;;  $ env %uid=500 %paths='("f0" "d0/f1")' ... -uid 500 -print-path
+;;  f0
+;;  $ env %fid='"0x200000404:0x1:0x0"' %uid=500 ... -uid 500 -print-fid
+;;  0x200000404:0x1:0x0
+;;  $ env %fid='"0x200000404:0x1:0x0"' %uid=500 %gid=501 ... -uid 500 -and -gid 501 -print-fid
+;;  0x200000404:0x1:0x0
+;;  $ env %fid='"0x200000404:0x1:0x0"' %uid=500 %gid=501 ... -uid 500 -and -gid 502 -print-fid
+;;  $
+;;  $ env %fid='"0x200000404:0x1:0x0"' %names='("f0" "f1")' %uid=500 %gid=501 ... -name f0 -a -name f1
+;;  0x200000404:0x1:0x0
+;;
+;;  $ env %gid=60 %paths='("f0")' ... -group games -exec echo {} \;
+;;  EXEC: "echo" "f0"
+;;
+;; NOTES
+;;
+;; Add %DEBUG=1 in the environment to enable debugging in this script.
+
+;; (use-modules (system base compile))
+(use-modules (ice-9 format))
+(use-modules (ice-9 popen)) ;; open-pipe*
+(use-modules (srfi srfi-1)) ;; any
+
+(define (mock-getenv str def)
+  (or (getenv str) def))
+
+(define MOCK-SYMBOL-ENV-PREFIX "%")
+(define MOCK-SYMBOLS '(fid dev ino mode nlink uid gid rdev size atime mtime ctime blocks links names paths projid))
+
+(define *mock-client-mount-path* (mock-getenv "%MOUNT" "/lustre/fs0042/client"))
+(define *mock-debug-enabled* (mock-getenv "%DEBUG" #f))
+
+(define (mock-assert-value symbol value)
+  (or value (error symbol "no value")))
+
+(define (mock-assert-exec-allowed args)
+  ;; Avoid exec-ing arbitrary commands.
+  (or (list? args)
+      (error args "not an argument list"))
+  (or (string=? (car args) "echo")
+      (error (car args) "exec not allowed")))
+
+(define (mock-attr-name symbol)
+  (string-append MOCK-SYMBOL-ENV-PREFIX (symbol->string symbol)))
+
+;; TODO Allow copying attrs from a reference file.
+;; regex
+;; quit
+
+(define-syntax mock-debug1
+  (syntax-rules ()
+    ((_ x) (if *mock-debug-enabled*
+              (format (current-error-port) "DEBUG: ~a => ~s\n" (quote x) x)))))
+
+(define (mock-attr-ref symbol)
+  ;; Return a thunk that converts symbol to prefixed environment
+  ;; variable name, gets environment variable value, passes it to read
+  ;; to get a scheme value (datum) and returns that value. (Note datum
+  ;; is not evaluated.)
+  ;;
+  ;; Examples:
+  ;;   If '%uid=42' is set in the environment then (uid) will return 42.
+  ;;
+  ;;  If %paths='("foo.txt" "link-to-foo.txt" ...)' is set ... then
+  ;;  (paths) will return the list ("foo.txt" "link-to-foo.txt" ...).
+  (lambda ()
+    (let* ((value-as-string-or-false (getenv (mock-attr-name symbol)))
+          (value-as-string (mock-assert-value symbol value-as-string-or-false))
+          (value (call-with-input-string value-as-string read)))
+      value)))
+
+;; These three defines are to prevent the warning:
+;;   'lipe_scan_guile_mock.scm:44:14: warning: possibly unbound variable `fid''
+;; when this script is compiled (before it gets executed).
+(define (fid) (mock-attr-ref 'fid))
+(define (uid) (mock-attr-ref 'uid))
+(define (gid) (mock-attr-ref 'gid))
+(define (mode) (mock-attr-ref 'mode))
+(define (names) (mock-attr-ref 'names))
+(define (paths) (mock-attr-ref 'paths))
+
+(for-each (lambda (symbol)
+           (eval `(define ,symbol (mock-attr-ref ',symbol))
+                 (interaction-environment)))
+         MOCK-SYMBOLS)
+
+(define (lipe-scan-client-mount-path)
+  *mock-client-mount-path*)
+
+(define (make-absolute-path path)
+  (string-append (lipe-scan-client-mount-path) "/" path))
+
+(define (call-with-absolute-path proc)
+  (any (lambda (path)
+        (proc (make-absolute-path path)))
+       (paths)))
+
+(define (call-with-relative-path proc)
+  (any (lambda (path)
+        (proc path))
+       (paths)))
+
+(define (absolute-path)
+  (call-with-absolute-path identity))
+
+(define (relative-path)
+  (call-with-relative-path identity))
+
+(define (name)
+  ;; Returns #f if file has no name.
+  (any identity (names)))
+
+(define (user)
+  ;; Return uid as string if uid does not name a user.
+  (catch #t
+       (lambda ()
+         (passwd:name (getpwuid (uid))))
+       (lambda args
+         (number->string (uid)))))
+
+(define (group)
+  ;; Return gid as string if gid does not name a group.
+  (catch #t
+       (lambda ()
+         (group:name (getgrgid (gid))))
+       (lambda args
+         (number->string (gid)))))
+
+;; Temporary shim definition.
+(define (fnmatch? pattern str)
+  (string=? pattern str))
+
+;; Temporary shim definition.
+(define (fnmatch-ci? pattern str)
+  (string-ci=? pattern str))
+
+;; Shims
+(define-inlinable (round-up-power-of-2 x m)
+  (1+ (logior (- x 1) (- m 1))))
+
+(define-inlinable (round-up x m)
+  (* (/ (+ x m -1) m) m))
+
+(define S_IFMT   #o0170000)
+(define S_IFDIR  #o0040000)
+(define S_IFCHR  #o0020000)
+(define S_IFBLK  #o0060000)
+(define S_IFREG  #o0100000)
+(define S_IFIFO  #o0010000)
+(define S_IFLNK  #o0120000)
+(define S_IFSOCK #o0140000)
+
+(define (type->char t)
+  (cond
+   ((= t S_IFDIR) #\d)
+   ((= t S_IFCHR) #\c)
+   ((= t S_IFBLK) #\b)
+   ((= t S_IFREG) #\f)
+   ((= t S_IFIFO) #\p)
+   ((= t S_IFLNK) #\l)
+   ((= t S_IFSOCK) #\s)))
+
+(define-inlinable (type)
+  (logand (mode) S_IFMT))
+
+(define (print-fid)
+  (format (current-output-port) "~a\n" (fid)))
+
+(define (make-exec-plus-pipe . args)
+  (mock-assert-exec-allowed args)
+  (apply open-pipe* OPEN_WRITE "xargs" "--no-run-if-empty" "--null" "--" args))
+
+(define (exec-plus-pipe-write pipe)
+  (call-with-absolute-path
+   (lambda (path)
+     (format pipe "~a~a" path #\x00))))
+
+(define (exec . args)
+  (mock-assert-exec-allowed args)
+  (apply system* args))
+
+(define (lipe-getopt-client-mount-path)
+  *mock-client-mount-path*)
+
+(define (lipe-getopt-required-attrs)
+  0)
+
+(define (lipe-getopt-thread-count)
+  1)
+
+(define (lipe-scan device client-mount-path policy required-attrs thread-count)
+  (policy))
+
+(let ((port (current-input-port)))
+  (let loop ((expr (read port)))
+    (if (not (eof-object? expr))
+       (begin (mock-debug1 expr)
+              (eval expr (interaction-environment))
+              (loop (read port))))))
diff --git a/lipe/src/lipe_find3/xmalloc.h b/lipe/src/lipe_find3/xmalloc.h
new file mode 100644 (file)
index 0000000..7358d88
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef _XMALLOC_H_
+#define _XMALLOC_H_
+#include <malloc.h>
+#include <string.h>
+
+#define xcalloc calloc /* FIXME */
+#define xstrdup strdup /* FIXME */
+
+static inline char *xstrndup(const char *str, size_t n)
+{
+       char *s = xstrdup(str);
+
+       if (n < strlen(s))
+               s[n] = '\0';
+
+       return s;
+}
+
+#endif
index 1e6dd4d..251f54b 100644 (file)
@@ -36,6 +36,7 @@ noinst_SCRIPTS += parallel-scale-nfsv3.sh parallel-scale-nfsv4.sh
 noinst_SCRIPTS += setup-cifs.sh parallel-scale-cifs.sh
 noinst_SCRIPTS += posix.sh sanity-scrub.sh scrub-performance.sh ha.sh pjdfstest.sh
 noinst_SCRIPTS += sanity-lfsck.sh lfsck-performance.sh sanity-lipe.sh
+noinst_SCRIPTS += sanity-lipe-find3.sh
 noinst_SCRIPTS += sanity-lipe-scan3.sh
 noinst_SCRIPTS += resolveip
 noinst_SCRIPTS += sanity-hsm.sh sanity-lsnapshot.sh sanity-pfl.sh sanity-flr.sh
diff --git a/lustre/tests/sanity-lipe-find3.sh b/lustre/tests/sanity-lipe-find3.sh
new file mode 100644 (file)
index 0000000..f1206b7
--- /dev/null
@@ -0,0 +1,928 @@
+#!/bin/bash
+
+ONLY=${ONLY:-"$*"}
+
+LUSTRE=${LUSTRE:-$(dirname $0)/..}
+. $LUSTRE/tests/test-framework.sh
+init_test_env $@
+. ${CONFIG:=$LUSTRE/tests/cfg/$NAME.sh}
+init_logging
+FAIL_ON_ERROR=false
+
+declare -r ROOT_FID="[0x200000007:0x1:0x0]"
+
+# bug number for skipped test:
+ALWAYS_EXCEPT="$SANITY_LIPE_FIND3_EXCEPT"
+# UPDATE THE COMMENT ABOVE WITH BUG NUMBERS WHEN CHANGING ALWAYS_EXCEPT!
+
+(( OSTCOUNT >= 2 )) || skip_env "need at least 2 OSTs"
+
+[[ $(facet_fstype mds1) = ldiskfs ]] || skip_env "need ldiskfs on MDS"
+
+! remote_mds_nodsh || skip_env "remote MDS with nodsh"
+! remote_ost_nodsh || skip_env "remote OSS with nodsh"
+
+# check if lipe_find3 is installed on MDS(s)
+for t in lipe_find3; do
+       do_nodes $(comma_list $(all_mdts_nodes)) "which $t" ||
+       skip_env "$t is not installed on MDS"
+done
+
+which jq || skip_env "jq is not installed"
+
+build_test_filter
+check_and_setup_lustre
+
+# Try to support running from the build directory.
+if [[ "$mds_HOST" == "$HOSTNAME" ]]; then
+       # Fixup GUILE_LOAD_PATH to support running from the build directory.
+       LIPE_SCAN3=$(which lipe_scan3)
+       LIPE_SCAN3_DIR=$(dirname "$LIPE_SCAN3")
+       if [[ "$LIPE_SCAN3_DIR" != /usr/bin ]]; then
+               export GUILE_LOAD_PATH="$GUILE_LOAD_PATH:$LIPE_SCAN3_DIR"
+       fi
+
+       LIPE_FIND3=$(which lipe_find3)
+       LIPE_FIND3_DIR=$(dirname "$LIPE_FIND3")
+       if [[ "$LIPE_FIND3_DIR" != /usr/bin ]]; then
+               export GUILE_LOAD_PATH="$GUILE_LOAD_PATH:$LIPE_FIND3_DIR"
+       fi
+fi
+
+mount_client_on_facet() {
+       local facet="$1"
+       local nodes=$(facets_nodes "$facet")
+       local node
+
+       for node in $nodes; do
+               if local_node $node; then
+                       continue
+               fi
+
+               zconf_mount_clients $node $MOUNT ||
+                       error "cannot mount client on node '$node' for facet '$facet'"
+
+               stack_trap "zconf_umount_clients $node $MOUNT"
+       done
+}
+
+function init_lipe_find3_env() {
+       # Check "$MOUNT" is a Lustre client mount point.
+       local fid=$($LFS path2fid "$MOUNT")
+       [[ "$fid" == "$ROOT_FID" ]] || error "'$MOUNT' is not a lustre client mount"
+
+       find "$MOUNT" -mindepth 1 -delete || error "cannot clean '$MOUNT'"
+       find "$MOUNT" -mindepth 1 | grep . && error "find -delete did not delete all files from '$MOUNT'"
+
+       mount_client_on_facet mds1
+
+       for file in "$@"; do
+               touch $file || error "cannot create $file"
+               index=$($LFS getstripe --mdt-index $file)
+               ((index == 0)) || error "file '$file' is not on MDT0000"
+       done
+
+       sync
+       sync
+}
+
+function lipe_find3_on() {
+       local facet="$1"
+       shift 1
+
+       sync
+       do_facet_vp "$facet" sync
+       do_facet_vp "$facet" lipe_find3 "$@"
+}
+
+function lipe_find3_facet() {
+       local facet="$1"
+       local device="$(facet_device "$facet")"
+       shift 1
+
+       lipe_find3_on "$facet" "$device" "$@"
+}
+
+function expect_stdout() {
+       "$@" | grep --quiet . || error "command '$*' should write to stdout"
+}
+
+function expect_no_stdout() {
+       "$@" | grep . && error "command '$*' should not write to stdout"
+       true
+}
+
+function expect_stderr() {
+       "$@" 2>&1 > /dev/null | grep --quiet . || error "command '$*' should write to stderr"
+}
+
+function expect_no_stderr() {
+       "$@" 2>&1 > /dev/null | grep . && error "command '$*' should not write to stderr"
+       true
+}
+
+function expect_success() {
+       "$@" > /dev/null || error "command '$*' failed"
+}
+
+function expect_failure() {
+       "$@" 2> /dev/null && error "command '$*' should fail"
+       true
+}
+
+function expect_print() {
+       expect_success "$@"
+       expect_stdout "$@"
+       expect_no_stderr "$@"
+}
+
+function expect_empty() {
+       expect_success "$@"
+       expect_no_stdout "$@"
+       expect_no_stderr "$@"
+}
+
+function expect_error() {
+       expect_failure "$@"
+       expect_no_stdout "$@"
+       expect_stderr "$@"
+}
+
+function expect1() {
+       # Will only DTRT with single line output due to how $(...)  and
+       # == work.
+       local str="$1"
+       local out
+       shift
+       out=$("$@")
+       [[ "$str" == "$out" ]] || error "$*: expected '$str', got '$out'"
+}
+
+test_0() {
+       expect_success true
+       expect_failure false
+       expect_no_stdout true
+       expect_no_stderr true
+       expect_stdout echo something
+       expect_stderr grep --barf
+       expect_print echo output
+       expect_empty true
+       expect_error grep --barf
+}
+run_test 0 "expect functions meet our expectations"
+
+test_10() {
+       expect_print lipe_find3_on mds1 -h
+       expect_print lipe_find3_on mds1 --help
+
+       expect_print lipe_find3_on mds1 -v
+       expect_print lipe_find3_on mds1 --version
+
+       expect_error lipe_find3_on mds1
+       expect_error lipe_find3_on mds1 --barf
+}
+run_test 10 "lipe_find3 option handling"
+
+test_11() {
+       expect_error lipe_find3_on mds1 /dev/null
+       expect_error lipe_find3_on mds1 /dev/zero
+       expect_error lipe_find3_on mds1 /dev/zapper/mds1_flakey
+       expect_error lipe_find3_on mds1 /dev/
+       expect_error lipe_find3_on mds1 ''
+       expect_error lipe_find3_on mds1
+}
+run_test 11 "lipe_find3 bad device handling"
+
+test_12() {
+       expect_error lipe_find3_facet mds1 -quux ZZZ
+       expect_error lipe_find3_facet mds1 -quux
+       expect_error lipe_find3_facet mds1 -size
+       expect_error lipe_find3_facet mds1 -quux -quit
+       expect_error lipe_find3_facet mds1 -not -quux -quit
+       expect_error lipe_find3_facet mds1 -quit -quux
+
+       expect_error lipe_find3_facet mds1 -name zalf zalf
+       expect_error lipe_find3_facet mds1 true
+       expect_error lipe_find3_facet mds1 name zalf
+       expect_error lipe_find3_facet mds1 -true name zalf
+       expect_error lipe_find3_facet mds1 name zalf -true
+       expect_error lipe_find3_facet mds1 true
+       expect_error lipe_find3_facet mds1 quux
+}
+run_test 12 "lipe_find3 bad tests"
+
+test_90() {
+       init_lipe_find3_env
+       mount_client_on_facet ost1
+
+       # Create some files to be deleted.
+       touch $MOUNT/f0
+       mkdir $MOUNT/d0
+       mkfifo $MOUNT/p0
+       # lfs mkdir -c ... $MOUNT/d1 ....
+       ln -s $MOUNT/f0 $MOUNT/l0
+       mknod $MOUNT/c0 c 1 3
+       # ...
+
+       # XXX Run once to get rid of auto-compilation message.
+       lipe_find3_facet mds1
+       lipe_find3_facet ost1
+
+       # Now delete those files.
+       init_lipe_find3_env
+       wait_delete_completed
+       expect_empty lipe_find3_facet mds1
+       expect_empty lipe_find3_facet ost1
+}
+run_test 90 "lipe_find3 on an empty FS"
+
+test_100() {
+       local facet=mds1
+       local device="$(facet_device $facet)"
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env
+       $LFS setstripe -i 0 "$file"
+       echo XXX > "$file"
+       fid=$($LFS path2fid "$file")
+
+       expect1 "$fid" lipe_find3_facet mds1
+       expect1 "$fid" lipe_find3_facet ost1
+}
+run_test 100 "lipe_find3 finds a file by MDT and OST"
+
+declare -r S_IFREG=0100000
+declare -a MODES=(0 1 0111 0222 0444 0555 0666 0777)
+
+test_101() {
+       # TODO
+       true
+}
+run_test 101 "lipe_find3 perm does the right thing"
+
+declare -a IDS=(
+       0
+       42
+       500
+       65534
+       65535
+       65536
+       2147483646 # (INT_MAX - 1)
+       2147483647 # (INT_MAX)
+       2147483648 # (INT_MAX + 1)
+       2177452800
+       4294967293 # (UINT_MAX - 2)
+)
+
+test_102() {
+       local file=$MOUNT/$tfile
+       local fid
+       local id
+       local user
+
+       init_lipe_find3_env
+       $LFS setstripe -i 0 -c 1 "$file"
+       fid=$($LFS path2fid "$file")
+       echo XXX > "$file"
+       sync
+
+       id=$(stat --format=%u $file) # uid
+       expect1 "$fid" lipe_find3_facet mds1 -user "$id"
+       expect1 "$fid" lipe_find3_facet ost1 -user "$id"
+
+       for id in "${IDS[@]}"; do
+               chown $id $file || error "cannot set UID to '$id'"
+               wait_delete_completed
+
+               expect1 "$fid" lipe_find3_facet mds1 -user "$id"
+               expect1 "$fid" lipe_find3_facet ost1 -user "$id"
+
+               expect1 "$fid" lipe_find3_facet mds1 -uid "$id"
+               expect1 "$fid" lipe_find3_facet ost1 -uid "$id"
+       done
+
+       user=sanityusr
+       chown $user $file || error "cannot set user to '$user'"
+       wait_delete_completed
+
+       expect1 "$fid" lipe_find3_facet mds1 -user "$user"
+       expect1 "$fid" lipe_find3_facet ost1 -user "$user"
+
+       id=$(id -u "$user")
+       expect1 "$fid" lipe_find3_facet mds1 -uid "$id"
+       expect1 "$fid" lipe_find3_facet ost1 -uid "$id"
+
+       expect_error lipe_find3_facet mds1 -user
+       expect_error lipe_find3_facet mds1 -user ''
+       expect_error lipe_find3_facet mds1 -user -QQQ
+       expect_error lipe_find3_facet mds1 -user QQQ
+       expect_error lipe_find3_facet mds1 -user 42QQQ
+}
+run_test 102 "lipe_find3 -user does the right thing"
+
+test_103() {
+       local file=$MOUNT/$tfile
+       local fid
+       local id
+       local group
+
+       init_lipe_find3_env
+       $LFS setstripe -i 0 -c 1 "$file"
+       fid=$($LFS path2fid "$file")
+       echo XXX > "$file"
+       sync
+
+       id=$(stat --format=%g $file) # gid
+       expect1 "$fid" lipe_find3_facet mds1 -group "$id"
+       expect1 "$fid" lipe_find3_facet ost1 -group "$id"
+
+       for id in "${IDS[@]}"; do
+               chown :$id $file || error "cannot set GID to '$id'"
+               wait_delete_completed
+
+               expect1 "$fid" lipe_find3_facet mds1 -group "$id"
+               expect1 "$fid" lipe_find3_facet ost1 -group "$id"
+
+               expect1 "$fid" lipe_find3_facet mds1 -gid "$id"
+               expect1 "$fid" lipe_find3_facet ost1 -gid "$id"
+       done
+
+       group=sanityusr
+       chown :$group $file || error "cannot set group to '$group'"
+       wait_delete_completed
+
+       expect1 "$fid" lipe_find3_facet mds1 -group "$group"
+       expect1 "$fid" lipe_find3_facet ost1 -group "$group"
+
+       id=$(id -u "$group")
+       expect1 "$fid" lipe_find3_facet mds1 -gid "$id"
+       expect1 "$fid" lipe_find3_facet ost1 -gid "$id"
+
+       expect_error lipe_find3_facet mds1 -group
+       expect_error lipe_find3_facet mds1 -group ''
+       expect_error lipe_find3_facet mds1 -group -QQQ
+       expect_error lipe_find3_facet mds1 -group QQQ
+       expect_error lipe_find3_facet mds1 -group 42QQQ
+}
+run_test 103 "lipe_find3 -group does the right thing"
+
+declare -a SIZES=(
+                0
+               42
+       2147483646 # (INT_MAX - 1)
+       2147483647 # (INT_MAX)
+       2147483648 # (INT_MAX + 1)
+       2177452800
+       4294967294 # (UINT_MAX - 1)
+       4294967295 # (UINT_MAX)
+       4294967296 # (UINT_MAX + 1)
+       4815162342
+       8589934591 # 0x1ffffffff
+       48151623420
+       481516234200
+)
+
+test_104() {
+       local facet=mds1
+       local device="$(facet_device $facet)"
+       local file=$MOUNT/$tfile
+       local fid
+       local size
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       # XXX find uses 512 byte default unity for size
+
+       expect1 "$fid" lipe_find3_facet mds1 -size 0
+       expect1 "$fid" lipe_find3_facet mds1 -size 0b
+       expect1 "$fid" lipe_find3_facet mds1 -size 0c
+       expect1 "$fid" lipe_find3_facet mds1 -size 0k
+       expect1 "$fid" lipe_find3_facet mds1 -size 0M
+       expect1 "$fid" lipe_find3_facet mds1 -size 0G
+
+       expect_empty lipe_find3_facet mds1 -size +0
+       expect_empty lipe_find3_facet mds1 -size +0b
+       expect_empty lipe_find3_facet mds1 -size +0c
+       expect_empty lipe_find3_facet mds1 -size +0k
+       expect_empty lipe_find3_facet mds1 -size +0M
+       expect_empty lipe_find3_facet mds1 -size +0G
+
+       expect_empty lipe_find3_facet mds1 -size -0
+       expect_empty lipe_find3_facet mds1 -size -0b
+       expect_empty lipe_find3_facet mds1 -size -0c
+       expect_empty lipe_find3_facet mds1 -size -0k
+       expect_empty lipe_find3_facet mds1 -size -0M
+       expect_empty lipe_find3_facet mds1 -size -0G
+
+       expect1 "$fid" lipe_find3_facet mds1 -size -1b
+       expect1 "$fid" lipe_find3_facet mds1 -size -1k
+       expect1 "$fid" lipe_find3_facet mds1 -size -1M
+       expect1 "$fid" lipe_find3_facet mds1 -size -1G
+
+       truncate "$file" 512
+       expect1 "$fid" lipe_find3_facet mds1 -size 1b
+
+       # Match finds quirky +/- rounding blah blah.
+       truncate "$file" 1023
+       expect_empty lipe_find3_facet mds1 -size -1k
+       expect1 "$fid" lipe_find3_facet mds1 -size 1k
+       expect_empty lipe_find3_facet mds1 -size +1k
+
+       truncate "$file" 1024
+       expect_empty lipe_find3_facet mds1 -size -1k
+       expect1 "$fid" lipe_find3_facet mds1 -size 1k
+       expect_empty lipe_find3_facet mds1 -size +1k
+
+       truncate "$file" 1025
+       expect_empty lipe_find3_facet mds1 -size 1k
+       expect1 "$fid" lipe_find3_facet mds1 -size +1k
+       expect1 "$fid" lipe_find3_facet mds1 -size 2k
+       expect_empty lipe_find3_facet mds1 -size -2k
+       expect_empty lipe_find3_facet mds1 -size +2k
+
+       truncate "$file" 1048576
+       expect1 "$fid" lipe_find3_facet mds1 -size 1M
+
+       truncate "$file" 1073741824
+       expect1 "$fid" lipe_find3_facet mds1 -size 1G
+
+       for size in "${SIZES[@]}"; do
+               $TRUNCATE $file $size
+               expect1 "$fid" lipe_find3_facet mds1 -size ${size}c
+       done
+
+       expect_error lipe_find3_facet mds1 -size
+       expect_error lipe_find3_facet mds1 -size QQQ
+       expect_error lipe_find3_facet mds1 -size ''
+       expect_error lipe_find3_facet mds1 -size 1Q
+       expect_error lipe_find3_facet mds1 -size -1Q
+       expect_error lipe_find3_facet mds1 -size +1Q
+
+       # expect_attr "$device" blocks 0
+}
+run_test 104 "lipe_find3 -size does the right thing"
+
+test_105() {
+       local facet=mds1
+       local device="$(facet_device $facet)"
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       expect_empty lipe_find3_facet mds1 -links 0
+       expect1 "$fid" lipe_find3_facet mds1 -links +0
+       expect1 "$fid" lipe_find3_facet mds1 -links 1
+       expect1 "$fid" lipe_find3_facet mds1 -links -2
+
+       ln $file $file-2
+       expect_empty lipe_find3_facet mds1 -links 1
+       expect1 "$fid" lipe_find3_facet mds1 -links 2
+
+       ln $file $file-3
+       expect_empty lipe_find3_facet mds1 -links 2
+       expect1 "$fid" lipe_find3_facet mds1 -links 3
+
+       expect_error lipe_find3_facet mds1 -links
+       expect_error lipe_find3_facet mds1 -links ''
+       expect_error lipe_find3_facet mds1 -links QQQ
+       expect_error lipe_find3_facet mds1 -links 42QQQ
+
+       # If we hold $file open and remove all three links then
+       # lipe_find3 will still return a link count of 1 because of
+       # the PENDING/$fid link.
+}
+run_test 105 "lipe_find3 -links does the right thing"
+
+test_106() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       id=$($LFS project "$file" | awk '{ print $1 }')
+       expect1 "$fid" lipe_find3_facet mds1 -projid "$id"
+
+       for id in "${IDS[@]}"; do
+               [[ $id =~ ^[0-9]+$ ]] || continue # exclude sanityusr
+
+               $LFS project -p "$id" "$file" || error "cannot set projid to '$id'"
+               expect1 "$fid" lipe_find3_facet mds1 -projid "$id"
+       done
+
+       expect_error lipe_find3_facet mds1 -projid
+       expect_error lipe_find3_facet mds1 -projid ''
+       expect_error lipe_find3_facet mds1 -projid QQQ
+}
+run_test 106 "lipe_find3 -projid does the right thing"
+
+test_107() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       expect1 "$fid" lipe_find3_facet mds1 -type f
+       expect1 "$fid" lipe_find3_facet mds1 -type f,b
+       expect1 "$fid" lipe_find3_facet mds1 -type f,b,d
+       expect1 "$fid" lipe_find3_facet mds1 -type b,c,d,p,f,l,s
+       expect1 "$fid" lipe_find3_facet mds1 -not -type d
+       expect1 "$fid" lipe_find3_facet mds1 -not -type b,c,d,p,l,s
+
+       expect_empty lipe_find3_facet mds1 -type d
+       expect_empty lipe_find3_facet mds1 -type b,c,d,p,l,s
+       expect_empty lipe_find3_facet mds1 -not -type b,c,d,p,f,l,s
+
+       expect_error lipe_find3_facet mds1 -type
+       # expect_error lipe_find3_facet mds1 -type '' FIXME
+       expect_error lipe_find3_facet mds1 -type q
+       expect_error lipe_find3_facet mds1 -type b,c,d,p,l,s,q
+
+}
+run_test 107 "lipe_find3 -type does the right thing"
+
+test_108() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       # -name '' fails with "lipe_find3: FATAL: at argument 2:
+       # missing argument to '-name'". find is OK with -name ''.
+       expect1 "$fid" lipe_find3_facet mds1 -name "$tfile"
+       expect1 "$fid" lipe_find3_facet mds1 -name "$tfile*"
+       expect1 "$fid" lipe_find3_facet mds1 -name "*$tfile"
+       expect1 "$fid" lipe_find3_facet mds1 -name "$tfile*"
+       expect_empty lipe_find3_facet mds1 -name "dagobert.txt"
+
+       mv "$file" "$MOUNT/zalf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "zalf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "*.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "zalf.?"
+       expect1 "$fid" lipe_find3_facet mds1 -name "[yz]alf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "[yz]*.x"
+       expect_empty lipe_find3_facet mds1 -name "dagobert.txt"
+
+       expect1 "$fid" lipe_find3_facet mds1 -iname "zalf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "*.x"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "zalf.?"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "[yz]alf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "[yz]*.x"
+       expect_empty lipe_find3_facet mds1 -iname "dagobert.txt"
+
+       expect1 "$fid" lipe_find3_facet mds1 -iname "ZALF.X"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "Zalf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "*.x"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "ZALF.?"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "[yz]ALF.x"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "[YZ]*.X"
+       expect_empty lipe_find3_facet mds1 -iname "dagobert.txt"
+       expect_empty lipe_find3_facet mds1 -iname "DAGOBERT.TXT"
+
+       ln "$MOUNT/zalf.x" "$MOUNT/schmerp.out"
+       expect1 "$fid" lipe_find3_facet mds1 -name "*.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "zalf.?"
+       expect1 "$fid" lipe_find3_facet mds1 -name "[yz]alf.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "[yz]*.x"
+       expect1 "$fid" lipe_find3_facet mds1 -name "schmerp.out"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "SCHMERP.out"
+       expect1 "$fid" lipe_find3_facet mds1 -iname "SCHMERP.OUT"
+
+       expect_error lipe_find3_facet mds1 -name
+}
+run_test 108 "lipe_find3 -name and -iname do the right thing"
+
+test_109() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       # TODO What does -path match.
+       true
+}
+run_test 109 "lipe_find3 -path and -ipath do the right thing"
+
+test_130() {
+       local file=$MOUNT/$tfile
+       local xtime
+       local xmin
+       local now
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       # -atime n means last accessed n * 24 hours ago.  When find
+        # figures out how many 24-hour periods ago the fil e was last
+        # accessed, any fractional part is ignored, so to match -atime
+        # +1, a file has to have been accessed at least two days ago.
+
+       for x in a m c; do
+               xtime="-${x}time"
+               xmin="-${x}min"
+
+               expect_empty lipe_find3_facet mds1 $xtime -0
+               expect1 "$fid" lipe_find3_facet mds1 $xtime 0
+               expect_empty lipe_find3_facet mds1 $xtime +0
+               expect1 "$fid" lipe_find3_facet mds1 $xtime -1
+               expect_empty lipe_find3_facet mds1 $xtime 1
+
+               expect_empty lipe_find3_facet mds1 $xmin -0
+               expect1 "$fid" lipe_find3_facet mds1 $xmin -10
+               expect_empty lipe_find3_facet mds1 $xmin 10
+               expect_empty lipe_find3_facet mds1 $xmin +10
+
+               if [[ $x == c ]]; then
+                       continue
+               fi
+
+               now=$(date +%s)
+               touch -$x --date=@$((now - 25 * 3600)) $file
+               expect_empty lipe_find3_facet mds1 $xtime 0
+               expect1 "$fid" lipe_find3_facet mds1 $xtime +0
+               expect_empty lipe_find3_facet mds1 $xtime -1
+               expect1 "$fid" lipe_find3_facet mds1 $xtime 1
+               expect_empty lipe_find3_facet mds1 $xtime +1
+               expect1 "$fid" lipe_find3_facet mds1 $xtime -2
+
+               expect_empty lipe_find3_facet mds1 $xtime -86400s
+               expect_empty lipe_find3_facet mds1 $xtime 86400s
+               expect1 "$fid" lipe_find3_facet mds1 $xtime +86400s
+
+               expect_empty lipe_find3_facet mds1 $xtime -1440m
+               expect_empty lipe_find3_facet mds1 $xtime 1440m
+               expect1 "$fid" lipe_find3_facet mds1 $xtime +1440m
+
+               expect_empty lipe_find3_facet mds1 $xmin -1440
+               expect_empty lipe_find3_facet mds1 $xmin 1440
+               expect1 "$fid" lipe_find3_facet mds1 $xmin +1440
+
+               expect_empty lipe_find3_facet mds1 $xtime -24h
+               expect_empty lipe_find3_facet mds1 $xtime 24h
+               expect1 "$fid" lipe_find3_facet mds1 $xtime +24h
+
+               expect_empty lipe_find3_facet mds1 $xtime -25h
+               expect1 "$fid" lipe_find3_facet mds1 $xtime 25h
+               expect_empty lipe_find3_facet mds1 $xtime +25h
+
+               expect_empty lipe_find3_facet mds1 $xtime -1d
+               expect1 "$fid" lipe_find3_facet mds1 $xtime 1d
+               expect_empty lipe_find3_facet mds1 $xtime +1d
+
+               expect1 "$fid" lipe_find3_facet mds1 $xtime -93600s
+               expect1 "$fid" lipe_find3_facet mds1 $xtime -1560m
+               expect1 "$fid" lipe_find3_facet mds1 $xtime -26h
+               expect1 "$fid" lipe_find3_facet mds1 $xtime -2d
+
+               now=$(date +%s)
+               touch -$x --date=@$((now - 30 * 86400 - 3600)) $file
+               expect_empty lipe_find3_facet mds1 $xtime 0
+               expect_empty lipe_find3_facet mds1 $xtime -30
+               expect1 "$fid" lipe_find3_facet mds1 $xtime 30
+               expect_empty lipe_find3_facet mds1 $xtime +30
+       done
+
+       expect_error lipe_find3_facet mds1 -atime
+       expect_error lipe_find3_facet mds1 -atime ''
+       expect_error lipe_find3_facet mds1 -atime QQQ
+       expect_error lipe_find3_facet mds1 -atime 42Q
+       expect_error lipe_find3_facet mds1 -atime +42Q
+       expect_error lipe_find3_facet mds1 -atime +
+       expect_error lipe_find3_facet mds1 -atime -
+}
+run_test 130 "lipe_find3 {a,m,c}{time,min} do the right thing"
+
+# 200 printing
+# print-file-fid
+# print-self-fid
+# print-json
+# print-absolute-path
+# print-relative-path
+
+# fprint
+# fprint0
+# printf
+# fprintf
+
+# 300 actions
+# No implicit print when there is an explicit action
+
+# exec ... {} \;
+# exec ... {} +
+test_300() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env
+
+       # Need quotes around ; to protect from do_facet.
+       # Does this work with ssh?
+
+       expect_empty lipe_find3_facet mds1 -exec echo {} \;
+       expect_empty lipe_find3_facet mds1 -exec echo x{}x \;
+       expect_empty lipe_find3_facet mds1 -exec $LFS path2fid {} \;
+       expect_empty lipe_find3_facet mds1 -exec cat {} \;
+
+       echo 4815162342 > "$file"
+       fid=$($LFS path2fid "$file")
+
+       expect_empty lipe_find3_facet mds1 -exec true \;
+       expect_empty lipe_find3_facet mds1 -exec true {} \;
+       expect1 "$file" lipe_find3_facet mds1 -exec echo {} \;
+       expect1 "$file $file" lipe_find3_facet mds1 -exec echo {} {} \;
+       expect1 "$file $file" lipe_find3_facet mds1 -exec echo "{} {}" \;
+       expect1 "${file}x" lipe_find3_facet mds1 -exec echo "{}x" \;
+       expect1 "x${file}x" lipe_find3_facet mds1 -exec echo "x{}x" \;
+       expect1 "$fid" lipe_find3_facet mds1 -exec $LFS path2fid {} \;
+       expect1 4815162342 lipe_find3_facet mds1 -exec cat {} \;
+
+       # $ find /mnt/lustre -exec zalp {} \;
+       # find: ‘zalp’: No such file or directory
+       # find: ‘zalp’: No such file or directory
+       # $ echo $?
+       # 0
+
+       expect_error lipe_find3_facet mds1 -exec cat
+       expect_error lipe_find3_facet mds1 -exec cat {}
+       expect_error lipe_find3_facet mds1 -exec cat { \;
+       expect_error lipe_find3_facet mds1 -exec cat xx{xxx \;
+}
+run_test 300 "lipe_find3 -exec does the right thing"
+
+test_301() {
+       local file=$MOUNT/$tfile
+       local fid
+       local count
+
+       init_lipe_find3_env
+
+       expect_empty lipe_find3_facet mds1 -exec echo {} +
+       expect_empty lipe_find3_facet mds1 -exec $LFS path2fid {} +
+       expect_empty lipe_find3_facet mds1 -exec cat {} +
+
+       echo 4815162342 > "$file"
+       echo 4815162342 > "$file"-2
+
+       expect_empty lipe_find3_facet mds1 -exec true {} +
+       count=$(lipe_find3_facet mds1 -exec cat {} + | grep --count 4815162342)
+       ((count == 2)) || error "expected count 2, got $count"
+
+       # {} must be present in -exec ... +
+       # {} must be the last argument in -exec ... +
+       # {} must occur by it self
+       # only one instance of {} is supported with -exec ... +
+       expect_error lipe_find3_facet mds1 -exec cat +
+       expect_error lipe_find3_facet mds1 -exec cat {} zzz +
+       expect_error lipe_find3_facet mds1 -exec cat {} {} +
+       expect_error lipe_find3_facet mds1 -exec cat x{}x +
+}
+run_test 301 "lipe_find3 -exec + does the right thing"
+
+test_350() {
+       local file=$MOUNT/$tfile
+       local dir=$MOUNT/$tdir
+
+       init_lipe_find3_env
+
+       expect_empty lipe_find3_facet mds1 -delete
+
+       echo 4815162342 > "$file"
+       echo 4815162342 > "$file"-2
+
+       expect_empty lipe_find3_facet mds1 -delete
+       if [[ -f "$file" ]] || [[ -f "$file"-2 ]]; then
+               error "$file and $file-2 were not deleted"
+       fi
+
+       lfs mkdir -i0 -c1 $dir
+       expect_empty lipe_find3_facet mds1 -delete
+       if [[ -d "$dir" ]]; then
+               error "$dir was not deleted"
+       fi
+
+       # ....
+       # expect_error lipe_find3_facet mds1 -delete
+}
+run_test 350 "lipe_find3 -delete does the right thing"
+
+test_400() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       expect1 "$fid" lipe_find3_facet mds1 -true
+       expect_empty lipe_find3_facet mds1 -false
+
+       expect1 "$fid" lipe_find3_facet mds1 -not -false
+       expect_empty lipe_find3_facet mds1 -not -true
+
+       expect1 "$fid" lipe_find3_facet mds1 \! -false
+       expect_empty lipe_find3_facet mds1 \! -true
+
+       expect1 "$fid" lipe_find3_facet mds1 -not -not -true
+       expect_empty lipe_find3_facet mds1 -not -not -false
+
+       # -and is implicit
+       expect1 "$fid" lipe_find3_facet mds1 -true -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -and -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -or -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -or -false
+       expect1 "$fid" lipe_find3_facet mds1 -false -or -true
+
+       expect1 "$fid" lipe_find3_facet mds1 -true -a -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -o -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -o -false
+       expect1 "$fid" lipe_find3_facet mds1 -false -o -true
+
+       # -and is implicit
+       expect_empty lipe_find3_facet mds1 -true -false
+       expect_empty lipe_find3_facet mds1 -false -false
+       expect_empty lipe_find3_facet mds1 -false -true
+
+       expect_empty lipe_find3_facet mds1 -true -and -false
+       expect_empty lipe_find3_facet mds1 -false -and -false
+       expect_empty lipe_find3_facet mds1 -false -and -true
+       expect_empty lipe_find3_facet mds1 -false -or -false
+
+       expect_empty lipe_find3_facet mds1 -true -a -false
+       expect_empty lipe_find3_facet mds1 -false -a -false
+       expect_empty lipe_find3_facet mds1 -false -a -true
+       expect_empty lipe_find3_facet mds1 -false -o -false
+
+       # -and has higher precedence than -or
+       # "X && Y || Z" means "(X && Y) || Z"
+       #
+       # The differences between "(X && Y) || Z" and "X && (Y || Z)"
+       # arises when the inputs are FTT or FFT.
+       # F && T || T => T
+       # F && F || T => T
+
+       expect1 "$fid" lipe_find3_facet mds1 -false -and -true -or -true
+       expect1 "$fid" lipe_find3_facet mds1 -false -and -false -or -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -or -false -and -true
+       expect1 "$fid" lipe_find3_facet mds1 -true -or -false -and -false
+
+       expect_error lipe_find3_facet mds1 -and
+       expect_error lipe_find3_facet mds1 -true -and
+       expect_error lipe_find3_facet mds1 -and -true
+       expect_error lipe_find3_facet mds1 -true -and -and -true
+
+       expect_error lipe_find3_facet mds1 -or
+       expect_error lipe_find3_facet mds1 -true -or
+       expect_error lipe_find3_facet mds1 -or -true
+       expect_error lipe_find3_facet mds1 -true -or -or -true
+
+       expect_error lipe_find3_facet mds1 -true -and -or -true
+       expect_error lipe_find3_facet mds1 -true -or -and -true
+}
+run_test 400 "lipe_find3 -true, -false, -and, -or do the right thing"
+
+test_401() {
+       local file=$MOUNT/$tfile
+       local fid
+
+       init_lipe_find3_env "$file"
+       fid=$($LFS path2fid "$file")
+
+       expect1 "$fid" lipe_find3_facet mds1 \( -true \)
+       expect_empty lipe_find3_facet mds1 \( -false \)
+
+       expect_empty lipe_find3_facet mds1 -false -and \( -true -or -true \)
+       expect_empty lipe_find3_facet mds1 -false -and \( -false -or -true \)
+
+       expect1 "$fid" lipe_find3_facet mds1 \( -true -true \)
+       expect_empty lipe_find3_facet mds1 \( -false -true \)
+
+       expect1 "$fid" lipe_find3_facet mds1 \! \( -false \)
+       expect_empty lipe_find3_facet mds1 \! \( -true \)
+
+       expect1 "$fid" lipe_find3_facet mds1 \( \! -false \)
+       expect_empty lipe_find3_facet mds1 \( \! -true \)
+
+       expect1 "$fid" lipe_find3_facet mds1 -false , -true
+       expect_empty lipe_find3_facet mds1 -true , -false
+
+       expect_error lipe_find3_facet mds1 \(
+       expect_error lipe_find3_facet mds1 \( -true
+       expect_error lipe_find3_facet mds1 -true \)
+
+       expect_error lipe_find3_facet mds1 -true ,
+       expect_error lipe_find3_facet mds1 , -true
+       expect_error lipe_find3_facet mds1 ,
+       expect_error lipe_find3_facet mds1 -true , , -true
+}
+run_test 401 "lipe_find3 parens and commas do the right thing"
+
+complete $SECONDS
+check_and_cleanup_lustre
+exit_status