############################################################################
#
#	File:     fset.icn
#
#	Subject:  Program to do set operations on file specifications
#
#	Author:   Thomas R. Hicks
#
#	Date:     June 10, 1988
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#   The UNIX shell provides for the specification of filenames
#  using ``wildcards''.  Each wildcard specification may be
#  thought of as defining a set of names (that is, those that
#  match the specification).  Fset allows the user to apply the
#  set operations of intersection, union, and difference to
#  these filename sets. The resultant list may then be used as
#  an argument to other shell commands.
#
#  Fset's argument is an expression composed of legal UNIX file
#  specifications, parenthesis, and the following set opera-
#  tors:
#
#	&&   intersection
#	++   union
#	--   difference
#
#  Because characters that have special meaning to the shell
#  occur frequently in the arguments used for fset, it is
#  advisable to quote the arguments consistently.
#
#  The use of fset is illustrated by the following examples:
#
#	fset 'g*--*.icn'
#
#  produces the list (set) of filenames for files beginning
#  with g, excluding those ending with .icn.
#
#  Similarly,
#
#	fset '*'
#
#  produces all files in the current directory excluding the .
#  and .. files.
#
#	fset '((*--*.icn)++c*)'
#  and
#
#	fset '(*--*.icn)++c*'
#
#  produces the complement of all filenames ending with .icn in
#  addition to all filenames beginning with c.
#
#	fset '(((c? && c*)))'
#
#  is a redundant, but legal, specification for all two-
#  character filenames that begin with c, while
#
#	fset '.*'
#
#  produces the set of filenames for all hidden files, exclud-
#  ing the . and ..  files.
#
#  Limitations:
#
#  Multiple command line arguments, formed by omitting the
#  quotes around the file set expression, are permitted.  Their
#  use is limited, however, since parentheses do not get past
#  the shell's command-line expansion.
#
#  Almost any legal file specification will work when enclosed
#  in quotes except that the simple grammar that is used cannot
#  handle blanks adjacent to parentheses.
#
#  File names that begin or end in ``questionable'' characters
#  such as *, ?, +, -, and &, probably will not work.
#
#  A file specification that, when interpreted by the shell,
#  produces no matching filename will be placed (unchanged) in
#  the result.
#
############################################################################
#
#  See also:  gcomp.icn
#
############################################################################
#
#  Requires:  UNIX
#
############################################################################

procedure main(args)
   local i, fyls, arglist
   if *args = 0 then return
   if *args > 1 then
      every i := 2 to *args do
         args[1] ||:= (" " || args[i])
   (arglist := parse(args[1])) |
      stop("Invalid file specification expression")
   case type(arglist) of {
      "string"	: fyls := mkfset(arglist)
      "list"	: fyls := exec(arglist)
      default	: stop("Main: bad type -can't happen")
      }
   fyls := sort(fyls)
   every write(!fyls," ")
end

procedure Exp()			# file spec expression parser
   local a
   suspend (a := [Factor(),=Op(),Factor()] & [a[2],a[1],a[3]]) |
      Factor() |
      (a := [="(",Exp(),=")"] & .a[2])
end

procedure Factor()		# file spec expression parser
   local a
   suspend (a := [Term(),=Op(),Term()] & [a[2],a[1],a[3]]) |
      Term() |
      (a := [="(",Factor(),=")"] & .a[2])
end

procedure Name()		# file spec name matcher
   static valid
   initial valid := ~'()'
   suspend (any(~valid) || fail) | tab(find(Op()) | many(valid))
end

procedure Non()			# file spec expression parser
   local a
   suspend a := [Name(),=Op(),Name()] & [a[2],a[1],a[3]]
end

procedure Op()			# file spec operation matcher
   suspend !["++","--","&&"]
end

procedure Term()		# file spec expression parser
   local a
   suspend (a := [="(",Non(),=")"] & .a[2]) |
           Name()
end

procedure bldfset(arg)		# build file set, excluding . and ..
   local line
   static dotfiles
   initial dotfiles := set([".",".."])
   line := read(open("echo " || arg,"rp"))
   return str2set(line,' ') -- dotfiles
end

procedure exec(lst)		# process file spec list recursively
   return setops(lst[1])(exec2(lst[2]),exec2(lst[3]))
end

procedure exec2(arg)		# helping procedure for exec
   case type(arg) of {
      "string"	: return mkfset(arg)
      "list"	: return exec(arg)
      default	: stop("exec2: can't happen")
      }
end

procedure mkfset(fspec)		# make file list from specification
   if fspec == "*" then
      fspec := "* .*"
   return bldfset(fspec)
end

procedure parse(str)		# top level of parsing procedures
   local res
   str ? (res := Exp() & pos(0)) | fail
   return res
end

procedure sdiff(f1,f2)		# set difference
   return f1 -- f2
end

procedure setops(op)		# return correct set operaton
   case op of {
      "++"	: return sunion
      "&&"	: return sinter
      "--"	: return sdiff
      }
end

procedure sinter(f1,f2)		# set intersection
   return f1 ** f2
end

procedure str2set(str,delim)	# convert delimited string into a set
   local fset, f
   fset := set()
   str ? {
      while f := (tab(upto(delim))) do {
         insert(fset,f)
         move(1)
         }
      if "" ~== (f := tab(0)) then
         insert(fset,f)
      }
   return fset
end

procedure sunion(f1,f2)		# set union
   return f1 ++ f2
end
