Example #1
1
File: Macro.java Project: bramk/bnd
  public String _range(String args[]) {
    verifyCommand(args, _rangeHelp, _rangePattern, 2, 3);
    Version version = null;
    if (args.length >= 3) version = new Version(args[2]);
    else {
      String v = domain.getProperty("@");
      if (v == null) return null;
      version = new Version(v);
    }
    String spec = args[1];

    Matcher m = RANGE_MASK.matcher(spec);
    m.matches();
    String floor = m.group(1);
    String floorMask = m.group(2);
    String ceilingMask = m.group(3);
    String ceiling = m.group(4);

    String left = version(version, floorMask);
    String right = version(version, ceilingMask);
    StringBuilder sb = new StringBuilder();
    sb.append(floor);
    sb.append(left);
    sb.append(",");
    sb.append(right);
    sb.append(ceiling);

    String s = sb.toString();
    VersionRange vr = new VersionRange(s);
    if (!(vr.includes(vr.getHigh()) || vr.includes(vr.getLow()))) {
      domain.error(
          "${range} macro created an invalid range %s from %s and mask %s", s, version, spec);
    }
    return sb.toString();
  }
Example #2
0
File: Macro.java Project: bramk/bnd
  String filter(String[] args, boolean include) {
    verifyCommand(args, String.format(_filterHelp, args[0]), null, 3, 3);

    Collection<String> list = new ArrayList<String>(Processor.split(args[1]));
    Pattern pattern = Pattern.compile(args[2]);

    for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
      if (pattern.matcher(i.next()).matches() == include) i.remove();
    }
    return Processor.join(list);
  }
Example #3
0
File: Macro.java Project: bramk/bnd
  private String doCommands(String key, Link source) {
    String[] args = commands.split(key);
    if (args == null || args.length == 0) return null;

    for (int i = 0; i < args.length; i++)
      if (args[i].indexOf('\\') >= 0) args[i] = args[i].replaceAll("\\\\;", ";");

    if (args[0].startsWith("^")) {
      String varname = args[0].substring(1).trim();

      Processor parent = source.start.getParent();
      if (parent != null) return parent.getProperty(varname);
      return null;
    }

    Processor rover = domain;
    while (rover != null) {
      String result = doCommand(rover, args[0], args);
      if (result != null) return result;

      rover = rover.getParent();
    }

    for (int i = 0; targets != null && i < targets.length; i++) {
      String result = doCommand(targets[i], args[0], args);
      if (result != null) return result;
    }

    return doCommand(this, args[0], args);
  }
Example #4
0
File: Macro.java Project: bramk/bnd
/**
 * Provide a macro processor. This processor can replace variables in strings based on a properties
 * and a domain. The domain can implement functions that start with a "_" and take args[], the names
 * of these functions are available as functions in the macro processor (without the _). Macros can
 * nest to any depth but may not contain loops. Add POSIX macros: ${#parameter} String length.
 * ${parameter%word} Remove smallest suffix pattern. ${parameter%%word} Remove largest suffix
 * pattern. ${parameter#word} Remove smallest prefix pattern. ${parameter##word} Remove largest
 * prefix pattern.
 */
public class Macro {
  Processor domain;
  Object targets[];
  boolean flattening;

  public Macro(Processor domain, Object... targets) {
    this.domain = domain;
    this.targets = targets;
    if (targets != null) {
      for (Object o : targets) {
        assert o != null;
      }
    }
  }

  public String process(String line, Processor source) {
    return process(line, new Link(source, null, line));
  }

  String process(String line, Link link) {
    StringBuilder sb = new StringBuilder();
    process(line, 0, '\u0000', '\u0000', sb, link);
    return sb.toString();
  }

  int process(CharSequence org, int index, char begin, char end, StringBuilder result, Link link) {
    StringBuilder line = new StringBuilder(org);
    int nesting = 1;

    StringBuilder variable = new StringBuilder();
    outer:
    while (index < line.length()) {
      char c1 = line.charAt(index++);
      if (c1 == end) {
        if (--nesting == 0) {
          result.append(replace(variable.toString(), link));
          return index;
        }
      } else if (c1 == begin) nesting++;
      else if (c1 == '\\' && index < line.length() - 1 && line.charAt(index) == '$') {
        // remove the escape backslash and interpret the dollar
        // as a
        // literal
        index++;
        variable.append('$');
        continue outer;
      } else if (c1 == '$' && index < line.length() - 2) {
        char c2 = line.charAt(index);
        char terminator = getTerminator(c2);
        if (terminator != 0) {
          index = process(line, index + 1, c2, terminator, variable, link);
          continue outer;
        }
      } else if (c1 == '.' && index < line.length() && line.charAt(index) == '/') {
        // Found the sequence ./
        if (index == 1 || Character.isWhitespace(line.charAt(index - 2))) {
          // make sure it is preceded by whitespace or starts at begin
          index++;
          variable.append(domain.getBase().getAbsolutePath());
          variable.append('/');
          continue outer;
        }
      }
      variable.append(c1);
    }
    result.append(variable);
    return index;
  }

  public static char getTerminator(char c) {
    switch (c) {
      case '(':
        return ')';
      case '[':
        return ']';
      case '{':
        return '}';
      case '<':
        return '>';
      case '\u00ab': // Guillemet double << >>
        return '\u00bb';
      case '\u2039': // Guillemet single
        return '\u203a';
    }
    return 0;
  }

  protected String replace(String key, Link link) {
    if (link != null && link.contains(key)) return "${infinite:" + link.toString() + "}";

    if (key != null) {
      key = key.trim();
      if (key.length() > 0) {
        Processor source = domain;
        String value = null;

        if (key.indexOf(';') < 0) {
          Instruction ins = new Instruction(key);
          if (!ins.isLiteral()) {
            SortedList<String> sortedList = SortedList.fromIterator(domain.iterator());
            StringBuilder sb = new StringBuilder();
            String del = "";
            for (String k : sortedList) {
              if (ins.matches(k)) {
                String v = replace(k, new Link(source, link, key));
                if (v != null) {
                  sb.append(del);
                  del = ",";
                  sb.append(v);
                }
              }
            }
            return sb.toString();
          }
        }
        while (value == null && source != null) {
          value = source.getProperties().getProperty(key);
          source = source.getParent();
        }

        if (value != null) return process(value, new Link(source, link, key));

        value = doCommands(key, link);
        if (value != null) return process(value, new Link(source, link, key));

        if (key != null && key.trim().length() > 0) {
          value = System.getProperty(key);
          if (value != null) return value;
        }
        if (!flattening && !key.equals("@"))
          domain.warning("No translation found for macro: " + key);
      } else {
        domain.warning("Found empty macro key");
      }
    } else {
      domain.warning("Found null macro key");
    }
    return "${" + key + "}";
  }

  /**
   * Parse the key as a command. A command consist of parameters separated by ':'.
   *
   * @param key
   * @return
   */
  static Pattern commands = Pattern.compile("(?<!\\\\);");

  private String doCommands(String key, Link source) {
    String[] args = commands.split(key);
    if (args == null || args.length == 0) return null;

    for (int i = 0; i < args.length; i++)
      if (args[i].indexOf('\\') >= 0) args[i] = args[i].replaceAll("\\\\;", ";");

    if (args[0].startsWith("^")) {
      String varname = args[0].substring(1).trim();

      Processor parent = source.start.getParent();
      if (parent != null) return parent.getProperty(varname);
      return null;
    }

    Processor rover = domain;
    while (rover != null) {
      String result = doCommand(rover, args[0], args);
      if (result != null) return result;

      rover = rover.getParent();
    }

    for (int i = 0; targets != null && i < targets.length; i++) {
      String result = doCommand(targets[i], args[0], args);
      if (result != null) return result;
    }

    return doCommand(this, args[0], args);
  }

  private String doCommand(Object target, String method, String[] args) {
    if (target == null) ; // System.err.println("Huh? Target should never be null " +
    // domain);
    else {
      String cname = "_" + method.replaceAll("-", "_");
      try {
        Method m = target.getClass().getMethod(cname, new Class[] {String[].class});
        return (String) m.invoke(target, new Object[] {args});
      } catch (NoSuchMethodException e) {
        // Ignore
      } catch (InvocationTargetException e) {
        if (e.getCause() instanceof IllegalArgumentException) {
          domain.error(
              "%s, for cmd: %s, arguments; %s",
              e.getCause().getMessage(), method, Arrays.toString(args));
        } else {
          domain.warning("Exception in replace: %s", e.getCause());
          e.getCause().printStackTrace();
        }
      } catch (Exception e) {
        domain.warning("Exception in replace: " + e + " method=" + method);
        e.printStackTrace();
      }
    }
    return null;
  }

  /**
   * Return a unique list where the duplicates are removed.
   *
   * @param args
   * @return
   */
  static String _uniqHelp = "${uniq;<list> ...}";

  public String _uniq(String args[]) {
    verifyCommand(args, _uniqHelp, null, 1, Integer.MAX_VALUE);
    Set<String> set = new LinkedHashSet<String>();
    for (int i = 1; i < args.length; i++) {
      Processor.split(args[i], set);
    }
    return Processor.join(set, ",");
  }

  public String _pathseparator(@SuppressWarnings("unused") String args[]) {
    return File.pathSeparator;
  }

  public String _separator(@SuppressWarnings("unused") String args[]) {
    return File.separator;
  }

  public String _filter(String args[]) {
    return filter(args, false);
  }

  public String _filterout(String args[]) {
    return filter(args, true);
  }

  static String _filterHelp = "${%s;<list>;<regex>}";

  String filter(String[] args, boolean include) {
    verifyCommand(args, String.format(_filterHelp, args[0]), null, 3, 3);

    Collection<String> list = new ArrayList<String>(Processor.split(args[1]));
    Pattern pattern = Pattern.compile(args[2]);

    for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
      if (pattern.matcher(i.next()).matches() == include) i.remove();
    }
    return Processor.join(list);
  }

  static String _sortHelp = "${sort;<list>...}";

  public String _sort(String args[]) {
    verifyCommand(args, _sortHelp, null, 2, Integer.MAX_VALUE);

    List<String> result = new ArrayList<String>();
    for (int i = 1; i < args.length; i++) {
      Processor.split(args[i], result);
    }
    Collections.sort(result);
    return Processor.join(result);
  }

  static String _joinHelp = "${join;<list>...}";

  public String _join(String args[]) {

    verifyCommand(args, _joinHelp, null, 1, Integer.MAX_VALUE);

    List<String> result = new ArrayList<String>();
    for (int i = 1; i < args.length; i++) {
      Processor.split(args[i], result);
    }
    return Processor.join(result);
  }

  static String _ifHelp = "${if;<condition>;<iftrue> [;<iffalse>] }";

  public String _if(String args[]) {
    verifyCommand(args, _ifHelp, null, 3, 4);
    String condition = args[1].trim();
    if (!condition.equalsIgnoreCase("false")) if (condition.length() != 0) return args[2];

    if (args.length > 3) return args[3];
    return "";
  }

  public String _now(@SuppressWarnings("unused") String args[]) {
    return new Date().toString();
  }

  public static final String _fmodifiedHelp =
      "${fmodified;<list of filenames>...}, return latest modification date";

  public String _fmodified(String args[]) throws Exception {
    verifyCommand(args, _fmodifiedHelp, null, 2, Integer.MAX_VALUE);

    long time = 0;
    Collection<String> names = new ArrayList<String>();
    for (int i = 1; i < args.length; i++) {
      Processor.split(args[i], names);
    }
    for (String name : names) {
      File f = new File(name);
      if (f.exists() && f.lastModified() > time) time = f.lastModified();
    }
    return "" + time;
  }

  public String _long2date(String args[]) {
    try {
      return new Date(Long.parseLong(args[1])).toString();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return "not a valid long";
  }

  public String _literal(String args[]) {
    if (args.length != 2)
      throw new RuntimeException("Need a value for the ${literal;<value>} macro");
    return "${" + args[1] + "}";
  }

  public String _def(String args[]) {
    if (args.length != 2) throw new RuntimeException("Need a value for the ${def;<value>} macro");

    return domain.getProperty(args[1], "");
  }

  /**
   * replace ; <list> ; regex ; replace
   *
   * @param args
   * @return
   */
  public String _replace(String args[]) {
    if (args.length != 4) {
      domain.warning("Invalid nr of arguments to replace " + Arrays.asList(args));
      return null;
    }

    String list[] = args[1].split("\\s*,\\s*");
    StringBuilder sb = new StringBuilder();
    String del = "";
    for (int i = 0; i < list.length; i++) {
      String element = list[i].trim();
      if (!element.equals("")) {
        sb.append(del);
        sb.append(element.replaceAll(args[2], args[3]));
        del = ", ";
      }
    }

    return sb.toString();
  }

  public String _warning(String args[]) {
    for (int i = 1; i < args.length; i++) {
      domain.warning(process(args[i]));
    }
    return "";
  }

  public String _error(String args[]) {
    for (int i = 1; i < args.length; i++) {
      domain.error(process(args[i]));
    }
    return "";
  }

  /**
   * toclassname ; <path>.class ( , <path>.class ) *
   *
   * @param args
   * @return
   */
  static String _toclassnameHelp =
      "${classname;<list of class names>}, convert class paths to FQN class names ";

  public String _toclassname(String args[]) {
    verifyCommand(args, _toclassnameHelp, null, 2, 2);
    Collection<String> paths = Processor.split(args[1]);

    List<String> names = new ArrayList<String>(paths.size());
    for (String path : paths) {
      if (path.endsWith(".class")) {
        String name = path.substring(0, path.length() - 6).replace('/', '.');
        names.add(name);
      } else if (path.endsWith(".java")) {
        String name = path.substring(0, path.length() - 5).replace('/', '.');
        names.add(name);
      } else {
        domain.warning(
            "in toclassname, "
                + args[1]
                + " is not a class path because it does not end in .class");
      }
    }
    return Processor.join(names, ",");
  }

  /**
   * toclassname ; <path>.class ( , <path>.class ) *
   *
   * @param args
   * @return
   */
  static String _toclasspathHelp =
      "${toclasspath;<list>[;boolean]}, convert a list of class names to paths";

  public String _toclasspath(String args[]) {
    verifyCommand(args, _toclasspathHelp, null, 2, 3);
    boolean cl = true;
    if (args.length > 2) cl = Boolean.valueOf(args[2]);

    Collection<String> names = Processor.split(args[1]);
    Collection<String> paths = new ArrayList<String>(names.size());
    for (String name : names) {
      String path = name.replace('.', '/') + (cl ? ".class" : "");
      paths.add(path);
    }
    return Processor.join(paths, ",");
  }

  public String _dir(String args[]) {
    if (args.length < 2) {
      domain.warning("Need at least one file name for ${dir;...}");
      return null;
    }
    String del = "";
    StringBuilder sb = new StringBuilder();
    for (int i = 1; i < args.length; i++) {
      File f = domain.getFile(args[i]);
      if (f.exists() && f.getParentFile().exists()) {
        sb.append(del);
        sb.append(f.getParentFile().getAbsolutePath());
        del = ",";
      }
    }
    return sb.toString();
  }

  public String _basename(String args[]) {
    if (args.length < 2) {
      domain.warning("Need at least one file name for ${basename;...}");
      return null;
    }
    String del = "";
    StringBuilder sb = new StringBuilder();
    for (int i = 1; i < args.length; i++) {
      File f = domain.getFile(args[i]);
      if (f.exists() && f.getParentFile().exists()) {
        sb.append(del);
        sb.append(f.getName());
        del = ",";
      }
    }
    return sb.toString();
  }

  public String _isfile(String args[]) {
    if (args.length < 2) {
      domain.warning("Need at least one file name for ${isfile;...}");
      return null;
    }
    boolean isfile = true;
    for (int i = 1; i < args.length; i++) {
      File f = new File(args[i]).getAbsoluteFile();
      isfile &= f.isFile();
    }
    return isfile ? "true" : "false";
  }

  public String _isdir(String args[]) {
    if (args.length < 2) {
      domain.warning("Need at least one file name for ${isdir;...}");
      return null;
    }
    boolean isdir = true;
    for (int i = 1; i < args.length; i++) {
      File f = new File(args[i]).getAbsoluteFile();
      isdir &= f.isDirectory();
    }
    return isdir ? "true" : "false";
  }

  public String _tstamp(String args[]) {
    String format = "yyyyMMddHHmm";
    long now = System.currentTimeMillis();
    TimeZone tz = TimeZone.getTimeZone("UTC");

    if (args.length > 1) {
      format = args[1];
    }
    if (args.length > 2) {
      tz = TimeZone.getTimeZone(args[2]);
    }
    if (args.length > 3) {
      now = Long.parseLong(args[3]);
    }
    if (args.length > 4) {
      domain.warning("Too many arguments for tstamp: " + Arrays.toString(args));
    }

    SimpleDateFormat sdf = new SimpleDateFormat(format);
    sdf.setTimeZone(tz);

    return sdf.format(new Date(now));
  }

  /**
   * Wildcard a directory. The lists can contain Instruction that are matched against the given
   * directory ${lsr;<dir>;<list>(;<list>)*} ${lsa;<dir>;<list>(;<list>)*}
   *
   * @author aqute
   */
  public String _lsr(String args[]) {
    return ls(args, true);
  }

  public String _lsa(String args[]) {
    return ls(args, false);
  }

  String ls(String args[], boolean relative) {
    if (args.length < 2)
      throw new IllegalArgumentException(
          "the ${ls} macro must at least have a directory as parameter");

    File dir = domain.getFile(args[1]);
    if (!dir.isAbsolute())
      throw new IllegalArgumentException(
          "the ${ls} macro directory parameter is not absolute: " + dir);

    if (!dir.exists())
      throw new IllegalArgumentException(
          "the ${ls} macro directory parameter does not exist: " + dir);

    if (!dir.isDirectory())
      throw new IllegalArgumentException(
          "the ${ls} macro directory parameter points to a file instead of a directory: " + dir);

    Collection<File> files = new ArrayList<File>(new SortedList<File>(dir.listFiles()));

    for (int i = 2; i < args.length; i++) {
      Instructions filters = new Instructions(args[i]);
      files = filters.select(files, true);
    }

    List<String> result = new ArrayList<String>();
    for (File file : files) result.add(relative ? file.getName() : file.getAbsolutePath());

    return Processor.join(result, ",");
  }

  public String _currenttime(@SuppressWarnings("unused") String args[]) {
    return Long.toString(System.currentTimeMillis());
  }

  /**
   * Modify a version to set a version policy. Thed policy is a mask that is mapped to a version.
   *
   * <pre>
   * +           increment
   * -           decrement
   * =           maintain
   * &tilde;           discard
   *
   * ==+      = maintain major, minor, increment micro, discard qualifier
   * &tilde;&tilde;&tilde;=     = just get the qualifier
   * version=&quot;[${version;==;${@}},${version;=+;${@}})&quot;
   * </pre>
   *
   * @param args
   * @return
   */
  static final String MASK_STRING = "[\\-+=~0123456789]{0,3}[=~]?";

  static final Pattern MASK = Pattern.compile(MASK_STRING);
  static final String _versionHelp =
      "${version;<mask>;<version>}, modify a version\n"
          + "<mask> ::= [ M [ M [ M [ MQ ]]]\n"
          + "M ::= '+' | '-' | MQ\n"
          + "MQ ::= '~' | '='";
  static final Pattern _versionPattern[] = new Pattern[] {null, null, MASK, Verifier.VERSION};

  public String _version(String args[]) {
    verifyCommand(args, _versionHelp, null, 2, 3);

    String mask = args[1];

    Version version = null;
    if (args.length >= 3) version = new Version(args[2]);

    return version(version, mask);
  }

  String version(Version version, String mask) {
    if (version == null) {
      String v = domain.getProperty("@");
      if (v == null) {
        domain.error(
            "No version specified for ${version} or ${range} and no implicit version ${@} either, mask=%s",
            mask);
        v = "0";
      }
      version = new Version(v);
    }

    StringBuilder sb = new StringBuilder();
    String del = "";

    for (int i = 0; i < mask.length(); i++) {
      char c = mask.charAt(i);
      String result = null;
      if (c != '~') {
        if (i == 3) {
          result = version.getQualifier();
        } else if (Character.isDigit(c)) {
          // Handle masks like +00, =+0
          result = String.valueOf(c);
        } else {
          int x = version.get(i);
          switch (c) {
            case '+':
              x++;
              break;
            case '-':
              x--;
              break;
            case '=':
              break;
          }
          result = Integer.toString(x);
        }
        if (result != null) {
          sb.append(del);
          del = ".";
          sb.append(result);
        }
      }
    }
    return sb.toString();
  }

  /**
   * Schortcut for version policy
   *
   * <pre>
   * -provide-policy : ${policy;[==,=+)}
   * -consume-policy : ${policy;[==,+)}
   * </pre>
   *
   * @param args
   * @return
   */
  static Pattern RANGE_MASK =
      Pattern.compile("(\\[|\\()(" + MASK_STRING + "),(" + MASK_STRING + ")(\\]|\\))");

  static String _rangeHelp =
      "${range;<mask>[;<version>]}, range for version, if version not specified lookyp ${@}\n"
          + "<mask> ::= [ M [ M [ M [ MQ ]]]\n"
          + "M ::= '+' | '-' | MQ\n"
          + "MQ ::= '~' | '='";
  static Pattern _rangePattern[] = new Pattern[] {null, RANGE_MASK};

  public String _range(String args[]) {
    verifyCommand(args, _rangeHelp, _rangePattern, 2, 3);
    Version version = null;
    if (args.length >= 3) version = new Version(args[2]);
    else {
      String v = domain.getProperty("@");
      if (v == null) return null;
      version = new Version(v);
    }
    String spec = args[1];

    Matcher m = RANGE_MASK.matcher(spec);
    m.matches();
    String floor = m.group(1);
    String floorMask = m.group(2);
    String ceilingMask = m.group(3);
    String ceiling = m.group(4);

    String left = version(version, floorMask);
    String right = version(version, ceilingMask);
    StringBuilder sb = new StringBuilder();
    sb.append(floor);
    sb.append(left);
    sb.append(",");
    sb.append(right);
    sb.append(ceiling);

    String s = sb.toString();
    VersionRange vr = new VersionRange(s);
    if (!(vr.includes(vr.getHigh()) || vr.includes(vr.getLow()))) {
      domain.error(
          "${range} macro created an invalid range %s from %s and mask %s", s, version, spec);
    }
    return sb.toString();
  }

  /**
   * System command. Execute a command and insert the result.
   *
   * @param args
   * @param help
   * @param patterns
   * @param low
   * @param high
   */
  public String system_internal(boolean allowFail, String args[]) throws Exception {
    verifyCommand(
        args,
        "${"
            + (allowFail ? "system-allow-fail" : "system")
            + ";<command>[;<in>]}, execute a system command",
        null,
        2,
        3);
    String command = args[1];
    String input = null;

    if (args.length > 2) {
      input = args[2];
    }

    Process process = Runtime.getRuntime().exec(command, null, domain.getBase());
    if (input != null) {
      process.getOutputStream().write(input.getBytes("UTF-8"));
    }
    process.getOutputStream().close();

    String s = IO.collect(process.getInputStream(), "UTF-8");
    int exitValue = process.waitFor();
    if (exitValue != 0) return exitValue + "";

    if (!allowFail && (exitValue != 0)) {
      domain.error("System command " + command + " failed with " + exitValue);
    }
    return s.trim();
  }

  public String _system(String args[]) throws Exception {
    return system_internal(false, args);
  }

  public String _system_allow_fail(String args[]) throws Exception {
    String result = "";
    try {
      result = system_internal(true, args);
    } catch (Throwable t) {
      /* ignore */
    }
    return result;
  }

  public String _env(String args[]) {
    verifyCommand(args, "${env;<name>}, get the environmet variable", null, 2, 2);

    try {
      return System.getenv(args[1]);
    } catch (Throwable t) {
      return null;
    }
  }

  /**
   * Get the contents of a file.
   *
   * @param in
   * @return
   * @throws IOException
   */
  public String _cat(String args[]) throws IOException {
    verifyCommand(args, "${cat;<in>}, get the content of a file", null, 2, 2);
    File f = domain.getFile(args[1]);
    if (f.isFile()) {
      return IO.collect(f);
    } else if (f.isDirectory()) {
      return Arrays.toString(f.list());
    } else {
      try {
        URL url = new URL(args[1]);
        return IO.collect(url, "UTF-8");
      } catch (MalformedURLException mfue) {
        // Ignore here
      }
      return null;
    }
  }

  public static void verifyCommand(
      String args[],
      @SuppressWarnings("unused") String help,
      Pattern[] patterns,
      int low,
      int high) {
    String message = "";
    if (args.length > high) {
      message = "too many arguments";
    } else if (args.length < low) {
      message = "too few arguments";
    } else {
      for (int i = 0; patterns != null && i < patterns.length && i < args.length; i++) {
        if (patterns[i] != null) {
          Matcher m = patterns[i].matcher(args[i]);
          if (!m.matches())
            message +=
                String.format(
                    "Argument %s (%s) does not match %s%n", i, args[i], patterns[i].pattern());
        }
      }
    }
    if (message.length() != 0) {
      StringBuilder sb = new StringBuilder();
      String del = "${";
      for (String arg : args) {
        sb.append(del);
        sb.append(arg);
        del = ";";
      }
      sb.append("}, is not understood. ");
      sb.append(message);
      throw new IllegalArgumentException(sb.toString());
    }
  }

  // Helper class to track expansion of variables
  // on the stack.
  static class Link {
    Link previous;
    String key;
    Processor start;

    public Link(Processor start, Link previous, String key) {
      this.previous = previous;
      this.key = key;
      this.start = start;
    }

    public boolean contains(String key) {
      if (this.key.equals(key)) return true;

      if (previous == null) return false;

      return previous.contains(key);
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      String del = "[";
      for (Link r = this; r != null; r = r.previous) {
        sb.append(del);
        sb.append(r.key);
        del = ",";
      }
      sb.append("]");
      return sb.toString();
    }
  }

  /**
   * Take all the properties and translate them to actual values. This method takes the set
   * properties and traverse them over all entries, including the default properties for that
   * properties. The values no longer contain macros.
   *
   * @return A new Properties with the flattened values
   */
  public Properties getFlattenedProperties() {
    // Some macros only work in a lower processor, so we
    // do not report unknown macros while flattening
    flattening = true;
    try {
      Properties flattened = new Properties();
      Properties source = domain.getProperties();
      for (Enumeration<?> e = source.propertyNames(); e.hasMoreElements(); ) {
        String key = (String) e.nextElement();
        if (!key.startsWith("_"))
          if (key.startsWith("-")) flattened.put(key, source.getProperty(key));
          else flattened.put(key, process(source.getProperty(key)));
      }
      return flattened;
    } finally {
      flattening = false;
    }
  }

  public static final String _fileHelp =
      "${file;<base>;<paths>...}, create correct OS dependent path";

  public String _osfile(String args[]) {
    verifyCommand(args, _fileHelp, null, 3, 3);
    File base = new File(args[1]);
    File f = Processor.getFile(base, args[2]);
    return f.getAbsolutePath();
  }

  public String _path(String args[]) {
    List<String> list = new ArrayList<String>();
    for (int i = 1; i < args.length; i++) {
      list.addAll(Processor.split(args[i]));
    }
    return Processor.join(list, File.pathSeparator);
  }

  public static Properties getParent(Properties p) {
    try {
      Field f = Properties.class.getDeclaredField("defaults");
      f.setAccessible(true);
      return (Properties) f.get(p);
    } catch (Exception e) {
      Field[] fields = Properties.class.getFields();
      System.err.println(Arrays.toString(fields));
      return null;
    }
  }

  public String process(String line) {
    return process(line, domain);
  }
}