/*
    This file is part of GNU APL, a free implementation of the
    ISO/IEC Standard 13751, "Programming Language APL, Extended"

    Copyright (C) 2008-2013  Dr. Jürgen Sauermann

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <string.h>

#include "CharCell.hh"
#include "ComplexCell.hh"
#include "Command.hh"
#include "Executable.hh"
#include "FloatCell.hh"
#include "IndexExpr.hh"
#include "IntCell.hh"
#include "Input.hh"
#include "IO_Files.hh"
#include "LibPaths.hh"
#include "main.hh"
#include "Nabla.hh"
#include "Output.hh"
#include "Parser.hh"
#include "Prefix.hh"
#include "Quad_TF.hh"
#include "StateIndicator.hh"
#include "Svar_DB.hh"
#include "Symbol.hh"
#include "Tokenizer.hh"
#include "UserFunction.hh"
#include "UserPreferences.hh"
#include "Value.icc"
#include "Workspace.hh"

#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

int Command::boxing_format = -1;

vector<Command::user_command> Command::user_commands;

//-----------------------------------------------------------------------------
void
Command::process_line()
{
UCS_string line = Input::get_line();   // get_line() removes leading whitespace
   process_line(line);
}
//-----------------------------------------------------------------------------
void
Command::process_line(UCS_string & line)
{
   line.remove_leading_and_trailing_whitespaces();
   if (line.size() == 0)   return;   // empty input line

   switch(line[0])
      {
         case UNI_ASCII_R_PARENT:      // regular command, e.g. )SI
              do_APL_command(COUT, line);
              if (line.size())   break;
              return;

         case UNI_ASCII_R_BRACK:       // debug command, e.g. ]LOG
              do_APL_command(CERR, line);
              if (line.size())   break;
              return;

         case UNI_NABLA:               // e.g. ∇FUN
              Nabla::edit_function(line);
              return;

         case UNI_ASCII_NUMBER_SIGN:   // e.g. # comment
         case UNI_COMMENT:             // e.g. ⍝ comment
              return;

        default: ;
      }

   do_APL_expression(line);
}
//-----------------------------------------------------------------------------
bool
Command::do_APL_command(ostream & out, UCS_string & line)
{
const UCS_string line1(line);   // the original line

   // split line into command and arguments
   //
UCS_string cmd;   // the command without arguments
int len = 0;
   line.copy_black(cmd, len);

UCS_string arg(line, len, line.size() - len);
vector<UCS_string> args;
   arg.copy_black_list(args);
   line.clear();
   if (!cmd.starts_iwith(")MORE")) 
      {
        // clear )MORE info unless command is )MORE
        //
        Workspace::more_error().clear();
      }

#define cmd_def(cmd_str, code, _arg) \
   if (cmd.starts_iwith(cmd_str)) { code; return true; }
#include "Command.def"

   // check for user defined commands...
   //
   loop(u, user_commands.size())
       {
         if (cmd.starts_iwith(user_commands[u].prefix))
            {
              do_USERCMD(out, line, line1, cmd, args, u);
              return true;
            }
       }

     out << "BAD COMMAND" << endl;
     return false;
}
//-----------------------------------------------------------------------------
void
Command::do_APL_expression(UCS_string & line)
{
   Workspace::more_error().clear();

Executable * statements = 0;
   try
      {
        statements = StatementList::fix(line, LOC);
      }
   catch (Error err)
      {
        COUT << "SYNTAX ERROR";
        if (Workspace::more_error().size())   COUT << "+";
        COUT << endl;
        if (err.get_error_line_2().size())
           {
             COUT << "      " << err.get_error_line_2() << endl
                  << "      " << err.get_error_line_3() << endl;
           }

        err.print(CERR);
        return;
      }
   catch (...)
      {
        CERR << "*** Command::process_line() caught other exception ***"
             << endl;
        return;
      }

   if (statements == 0)
      {
        COUT << "main: Parse error." << endl;
        return;
      }

   // At this point, the user command was parsed correctly.
   // check for Escape (→)
   //
   {
     const Token_string & body = statements->get_body();
     if (body.size() == 3                &&
         body[0].get_tag() == TOK_ESCAPE &&
         body[1].get_Class() == TC_END   &&
         body[2].get_tag() == TOK_RETURN_STATS)
        {
          delete statements;

          // remove all SI entries up to (including) the next immediate
          // execution context
          //
          for (bool goon = true; goon;)
              {
                StateIndicator * si = Workspace::SI_top();
                if (si == 0)   break;   // SI empty

                const Executable * exec = si->get_executable();
                Assert(exec);
                goon = exec->get_parse_mode() != PM_STATEMENT_LIST;
                si->escape();   // pop local vars of user defined functions
                Workspace::pop_SI(LOC);
              }
          return;
        }
   }

// statements->print(CERR);

   // push a new context for the statements.
   //
   Workspace::push_SI(statements, LOC);

   for (;;)
       {
         //
         // NOTE that the entire SI may change while executing this loop.
         // We should therefore avoid references to SI entries.
         //
         Token token = Workspace::SI_top()->get_executable()->execute_body();

// Q(token)

         // start over if execution has pushed a new context
         //
         if (token.get_tag() == TOK_SI_PUSHED)   continue;

         // maybe call EOC handler and repeat if true returned
         //
       check_EOC:
         if (Workspace::SI_top()->call_eoc_handler(token))
            continue;

         // the far most frequent cases are TC_VALUE and TOK_VOID
         // so we handle them first.
         //
         if (token.get_Class() == TC_VALUE || token.get_tag() == TOK_VOID )
            {
              if (Workspace::SI_top()->get_executable()
                             ->get_parse_mode() == PM_STATEMENT_LIST)
                 {
                   if (attention_raised)
                      {
                        attention_raised = false;
                        interrupt_raised = false;
                        ATTENTION;
                      }

                   break;   // will return to calling context
                 }

              Workspace::pop_SI(LOC);

              // we are back in the calling SI. There should be a TOK_SI_PUSHED
              // token at the top of stack. Replace it with the result from
              //  the called (just poped) SI.
              //
              {
                Prefix & prefix =
                         Workspace::SI_top()->get_prefix();
                Assert(prefix.at0().get_tag() == TOK_SI_PUSHED);

                copy_1(prefix.tos().tok, token, LOC);
              }
              if (attention_raised)
                 {
                   attention_raised = false;
                   interrupt_raised = false;
                   ATTENTION;
                 }

              continue;
            }

         if (token.get_tag() == TOK_BRANCH)
            {
              StateIndicator * si = Workspace::SI_top_fun();

              if (si == 0)
                 {
                    Workspace::more_error() = UCS_string(
                          "branch back into function (→N) without "
                            "suspended function");
                    SYNTAX_ERROR;   // →N without function,
                 }

              // pop contexts above defined function
              //
              while (si != Workspace::SI_top())   Workspace::pop_SI(LOC);

              const Function_Line line = Function_Line(token.get_int_val());

              if (line == Function_Retry)   si->retry(LOC);
              else                          si->goon(line, LOC);
              continue;
            }

         if (token.get_tag() == TOK_ESCAPE)
            {
              // remove all SI entries up to (including) the next immediate
              // execution context
              //
              for (bool goon = true; goon;)
                  {
                    StateIndicator * si = Workspace::SI_top();
                    if (si == 0)   break;   // SI empty

                    const Executable * exec = si->get_executable();
                    Assert(exec);
                    goon = exec->get_parse_mode() != PM_STATEMENT_LIST;
                    si->escape();   // pop local vars of user defined functions
                    Workspace::pop_SI(LOC);
              }
              return;

              Assert(0 && "not reached");
            }

         if (token.get_tag() == TOK_ERROR)
            {
              // clear attention and interrupt flags
              //
              attention_raised = false;
              interrupt_raised = false;

              // check for safe execution mode. Entries in safe execution mode
              // can be far above the current SI entry. The EOC handler will
              // unroll the SI stack.
              //
              for (StateIndicator * si = Workspace::SI_top();
                   si; si = si->get_parent())
                  {
                    if (si->get_safe_execution())
                       {
                         // pop SI entries above the entry with safe_execution
                         //
                         while (Workspace::SI_top() != si)
                            {
                              Workspace::pop_SI(LOC);
                            }

                         goto check_EOC;
                       }
                  }

              // if suspend is not allowed then pop all SI entries that
              // don't allow suspend
              //
              if (Workspace::SI_top()->get_executable()->cannot_suspend())
                 {
                    Error error = Workspace::SI_top()->get_error();
                    while (Workspace::SI_top()->get_executable()
                                              ->cannot_suspend())
                       {
                         Workspace::pop_SI(LOC);
                       }

                   if (Workspace::SI_top())
                      {
                        Workspace::SI_top()->get_error() = error;
                      }
                 }

              Workspace::get_error()->print(CERR);
              if (Workspace::SI_top()->get_level() == 0)
                 {
                   Value::erase_stale(LOC);
                   IndexExpr::erase_stale(LOC);
                 }
              return;
            }

         // we should not come here.
         //
         Q1(token)  Q1(token.get_Class())  Q1(token.get_tag())  FIXME;
       }

   // pop the context for the statements
   //
   Workspace::pop_SI(LOC);
}
//-----------------------------------------------------------------------------
void 
Command::cmd_BOXING(ostream & out, const UCS_string & arg)
{
int format = arg.atoi();

   if (arg.starts_iwith("OFF"))   format = -1;

   switch (format)
      {
        case -1:
        case  2:
        case  3:
        case  4:
        case  7:
        case  8:
        case  9: boxing_format = format;
                 return;

        default:   out << "Bad ]BOXING parameter " << arg
                       << " (valid values are: OFF, 2, 3, 4, 7, 8, and 9)"
                       << endl;
      }
}
//-----------------------------------------------------------------------------
void 
Command::cmd_CHECK(ostream & out)
{
   // erase stale functions from failed ⎕EX
   //
   {
     bool erased = false;
     int stale = Workspace::cleanup_expunged(CERR, erased);
     if (stale)
        {
          CERR << "WARNING - " << stale << " stale functions ("
               << (erased ? "" : "not ") << "erased)" << endl;
        }
     else CERR << "OK      - no stale functions" << endl;
   }

   {
     const int stale = Value::print_stale(CERR);
     if (stale)
        {
          char cc[200];
          snprintf(cc, sizeof(cc), "ERROR   - %d stale values", stale);
          out << cc << endl;
        }
     else out << "OK      - no stale values" << endl;
   }
   {
     const int stale = IndexExpr::print_stale(CERR);
     if (stale)
        {
          char cc[200];
          snprintf(cc, sizeof(cc), "ERROR   - %d stale indices", stale);
          out << cc << endl;
        }
     else out << "OK      - no stale indices" << endl;
   }
}
//-----------------------------------------------------------------------------
void 
Command::cmd_CONTINUE(ostream & out)
{
   Workspace::wsid(out, UCS_string("CONTINUE"));

vector<UCS_string> vcont;
   Workspace::save_WS(out, vcont);
   cmd_OFF(0);
}
//-----------------------------------------------------------------------------
void 
Command::cmd_DROP(ostream & out, const vector<UCS_string> & lib_ws)
{
   // )DROP wsname
   // )DROP libnum wsname


   // check number of arguments (1 or 2)
   //
   if (lib_ws.size() == 0)   // missing argument
      {
        out << "BAD COMMAND+" << endl;
        Workspace::more_error() = "missing workspace name in command )DROP";
        return;
      }

   if (lib_ws.size() > 2)   // too many arguments
      {
        out << "BAD COMMAND+" << endl;
        Workspace::more_error() = "too many parameters in command )DROP";
        return;
      }

   // at this point, lib_ws.size() is 1 or 2. If 2 then the first
   // is the lib number
   //
LibRef libref = LIB_NONE;
UCS_string wname = lib_ws.back();
   if (lib_ws.size() == 2)   libref = (LibRef)(lib_ws.front().atoi());

UTF8_string filename = LibPaths::get_lib_filename(libref, wname, true,
                                                  ".xml", ".apl");

int result = unlink(filename.c_str());
   if (result)
      {
        out << wname << " NOT DROPPED: " << strerror(errno) << endl;
        UTF8_ostream more;
        more << "could not unlink file " << filename;
        Workspace::more_error() = UCS_string(more.get_data());
      }
}
//-----------------------------------------------------------------------------
void
Command::cmd_ERASE(ostream & out, vector<UCS_string> & args)
{
   Workspace::erase_symbols(CERR, args);
}
//-----------------------------------------------------------------------------
void 
Command::cmd_KEYB(ostream & out, const UCS_string & arg)
{
const int layout = arg.atoi();

   switch(layout)
      {
        case 0:

   out << "US Keyboard Layout:\n"
                              "\n"
"╔════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦═════════╗\n"
"║ ~  ║ !⌶ ║ @⍫ ║ #⍒ ║ $⍋ ║ %⌽ ║ ^⍉ ║ &⊖ ║ *⍟ ║ (⍱ ║ )⍲ ║ _! ║ +⌹ ║         ║\n"
"║ `◊ ║ 1¨ ║ 2¯ ║ 3< ║ 4≤ ║ 5= ║ 6≥ ║ 7> ║ 8≠ ║ 9∨ ║ 0∧ ║ -× ║ =÷ ║ BACKSP  ║\n"
"╠════╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦══════╣\n"
"║       ║ Q  ║ W⍹ ║ E⋸ ║ R  ║ T⍨ ║ Y¥ ║ U  ║ I⍸ ║ O⍥ ║ P⍣ ║ {⍞ ║ }⍬ ║  |⊣  ║\n"
"║  TAB  ║ q? ║ w⍵ ║ e∈ ║ r⍴ ║ t∼ ║ y↑ ║ u↓ ║ i⍳ ║ o○ ║ p⋆ ║ [← ║ ]→ ║  \\⊢  ║\n"
"╠═══════╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩══════╣\n"
"║ (CAPS   ║ A⍶ ║ S  ║ D  ║ F  ║ G  ║ H⍙ ║ J⍤ ║ K  ║ L⌷ ║ :≡ ║ \"≢ ║         ║\n"
"║  LOCK)  ║ a⍺ ║ s⌈ ║ d⌊ ║ f_ ║ g∇ ║ h∆ ║ j∘ ║ k' ║ l⎕ ║ ;⍎ ║ '⍕ ║ RETURN  ║\n"
"╠═════════╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═════════╣\n"
"║             ║ Z  ║ Xχ ║ C¢ ║ V  ║ B£ ║ N  ║ M  ║ <⍪ ║ >⍙ ║ ?⍠ ║          ║\n"
"║  SHIFT      ║ z⊂ ║ x⊃ ║ c∩ ║ v∪ ║ b⊥ ║ n⊤ ║ m| ║ ,⍝ ║ .⍀ ║ /⌿ ║  SHIFT   ║\n"
"╚═════════════╩════╩════╩════╩════╩════╩════╩════╩════╩════╩════╩══════════╝\n"
   << endl;
        break;

        case 1:
   out << "US Keyboard Layout 1:\n"
                                "\n"
"╔════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦════╦═════════╗\n"
"║ ~  ║ !⌶ ║ @⍫ ║ #⍒ ║ $⍋ ║ %⌽ ║ ^⍉ ║ &⊖ ║ *⍟ ║ (⍱ ║ )⍲ ║ _! ║ +⌹ ║         ║\n"
"║ `◊ ║ 1¨ ║ 2¯ ║ 3< ║ 4≤ ║ 5= ║ 6≥ ║ 7> ║ 8≠ ║ 9∨ ║ 0∧ ║ -× ║ =÷ ║ BACKSP  ║\n"
"╠════╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦═╩══╦══════╣\n"
"║       ║ Q  ║ W⍹ ║ E⍷ ║ R  ║ T⍨ ║ Y¥ ║ U  ║ I⍸ ║ O⍥ ║ P⍣ ║ {⍞ ║ }⍬ ║  |⊣  ║\n"
"║  TAB  ║ q? ║ w⍵ ║ e∈ ║ r⍴ ║ t∼ ║ y↑ ║ u↓ ║ i⍳ ║ o○ ║ p⋆ ║ [← ║ ]→ ║  \\⊢  ║\n"
"╠═══════╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩═╦══╩══════╣\n"
"║ (CAPS   ║ A⍶ ║ S  ║ D  ║ F  ║ G  ║ H⍙ ║ J⍤ ║ K  ║ L⌷ ║ :≡ ║ \"≢ ║         ║\n"
"║  LOCK)  ║ a⍺ ║ s⌈ ║ d⌊ ║ f_ ║ g∇ ║ h∆ ║ j∘ ║ k' ║ l⎕ ║ ;⍎ ║ '⍕ ║ RETURN  ║\n"
"╠═════════╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═══╦╩═════════╣\n"
"║             ║ Z  ║ Xχ ║ C¢ ║ V  ║ B£ ║ N  ║ M  ║ <⍪ ║ >⍙ ║ ?⍠ ║          ║\n"
"║  SHIFT      ║ z⊂ ║ x⊃ ║ c∩ ║ v∪ ║ b⊥ ║ n⊤ ║ m| ║ ,⍝ ║ .⍀ ║ /⌿ ║  SHIFT   ║\n"
"╚═════════════╩════╩════╩════╩════╩════╩════╩════╩════╩════╩════╩══════════╝\n"
   << endl;
        break;

        default: out << "*** unknown keyboard layout " << layout << endl;
      }
}
//-----------------------------------------------------------------------------
void 
Command::cmd_HELP(ostream & out)
{
   out << "Commands are:" << endl;
#define cmd_def(cmd_str, _cod, arg) \
   CERR << "      " cmd_str " " #arg << endl;
#include "Command.def"
   out << endl;

   if (user_commands.size())
      {
        out << "User defined commands:" << endl;
        loop(u, user_commands.size())
           {
             out << "      " << user_commands[u].prefix << " → ";
             if (user_commands[u].mode)   out << "A ";
             out << user_commands[u].apl_function << " B"
                 << " (mode " << user_commands[u].mode << ")" << endl;
           }
      }
}
//-----------------------------------------------------------------------------
void
Command::cmd_HOST(ostream & out, const UCS_string & arg)
{
   if (uprefs.safe_mode)
      {
        out << ")HOST command not allowed in safe mode." << endl;
        return;
      }

UTF8_string host_cmd(arg);
FILE * pipe = popen(host_cmd.c_str(), "r");
   if (pipe == 0)   // popen failed
      {
        out << ")HOST command failed: " << strerror(errno) << endl;
        return;
      }

   for (;;)
       {
         const int cc = fgetc(pipe);
         if (cc == EOF)   break;
         out << (char)cc;
       }

int result = pclose(pipe);
   out << endl << IntCell(result) << endl;
}
//-----------------------------------------------------------------------------
void
Command::cmd_IN(ostream & out, vector<UCS_string> & args, bool protection)
{
   if (args.size() == 0)
      {
        out << "BAD COMMAND" << endl;
        Workspace::more_error() =
                   UCS_string("missing filename in command )IN");
        return;
      }

UCS_string fname = args.front();
   args.erase(args.begin());

UTF8_string filename = LibPaths::get_lib_filename(LIB_NONE, fname, true,
                                                  ".atf", 0);

FILE * in = fopen(filename.c_str(), "r");
   if (in == 0)   // open failed: try filename.atf unless already .atf
      {
        UTF8_string fname_utf8(fname);
        CERR << ")IN " << fname_utf8.c_str()
             << " failed: " << strerror(errno) << endl;

        char cc[200];
        snprintf(cc, sizeof(cc),
                 "command )IN: could not open file %s for reading: %s",
                 fname_utf8.c_str(), strerror(errno));
        Workspace::more_error() = UCS_string(cc);
        return;
      }

UTF8 buffer[80];
int idx = 0;

transfer_context tctx(protection);

   for (;;)
      {
        const int cc = fgetc(in);
        if (cc == EOF)   break;
        if (idx == 0 && cc == 0x0A)   // optional LF
           {
             // CERR << "CRLF" << endl;
             continue;
           }

        if (idx < 80)
           {
              if (idx < 72)   buffer[idx++] = cc;
              else            buffer[idx++] = 0;
             continue;
           }

        if (cc == 0x0D || cc == 0x15)   // ASCII or EBCDIC
           {
             tctx.is_ebcdic = (cc == 0x15);
             tctx.process_record(buffer, args);

             idx = 0;
             ++tctx.recnum;
             continue;
           }

        CERR << "BAD record charset (neither ASCII nor EBCDIC)" << endl;
        break;
      }

   fclose(in);
}
//-----------------------------------------------------------------------------
void 
Command::cmd_LIBS(ostream & out, const vector<UCS_string> & lib_ref)
{
   // Command is:
   //
   // )LIB path           (set root to path)
   // )LIB                (display root and path states)
   //
   if (lib_ref.size() > 0)   // set path
      {
        UTF8_string utf(lib_ref[0]);
        LibPaths::set_APL_lib_root(utf.c_str());
        out << "LIBRARY ROOT SET TO " << lib_ref[0] << endl;
        return;
      }

   out << "Library root: " << LibPaths::get_APL_lib_root() << 
"\n"
"\n"
"Library reference number mapping:\n"
"\n"
"---------------------------------------------------------------------------\n"
"Ref Conf  Path                                                State   Err\n"
"---------------------------------------------------------------------------\n";
         

   loop(d, 10)
       {
          UTF8_string path = LibPaths::get_lib_dir((LibRef)d);
          out << " " << d << " ";
          switch(LibPaths::get_cfg_src((LibRef)d))
             {
                case LibPaths::LibDir::CS_NONE:      out << "NONE" << endl;
                                                     continue;
                case LibPaths::LibDir::CS_ENV:       out << "ENV   ";   break;
                case LibPaths::LibDir::CS_ARGV0:     out << "BIN   ";   break;
                case LibPaths::LibDir::CS_PREF_SYS:  out << "PSYS  ";   break;
                case LibPaths::LibDir::CS_PREF_HOME: out << "PUSER ";   break;
             }

        out << left << setw(52) << path.c_str();
        DIR * dir = opendir(path.c_str());
        if (dir)   { out << " present" << endl;   closedir(dir); }
        else       { out << " missing (" << errno << ")" << endl; }
      }

   out <<
"===========================================================================\n" << endl;
}
//-----------------------------------------------------------------------------
DIR *
Command::open_LIB_dir(UTF8_string & path, ostream & out, const UCS_string & arg)
{
   // arg is '' or '0'-'9' or path from:
   //
   // 1.  )LIB          meaning )LIB 0
   // 2.  )LIB 1
   // 3.  )LIB directory-name
   //
   if (arg.size() == 0)   // case 1.
      {
        path = LibPaths::get_lib_dir(LIB0);
      }
   else if (arg.size() == 1 && Avec::is_digit((Unicode)arg[0]))   // case 2.
      {
        path = LibPaths::get_lib_dir((LibRef)(arg.atoi()));
      }
   else  // case 3.
      {
        path = UTF8_string(arg);
      }

   // follow symbolic links...
   //
   loop(depth, 20)
       {
         char buffer[FILENAME_MAX + 1];
         const ssize_t len = readlink(path.c_str(), buffer, FILENAME_MAX);
         if (len <= 0)   break;   // not a symlink

         buffer[len] = 0;
         if (buffer[0] == '/')   // absolute path
            {
              path = UTF8_string(buffer);
            }
          else                   // relative path
            {
              path.append((UTF8)'/');
              path.append(UTF8_string(buffer));
            }
       }

DIR * dir = opendir(path.c_str());

   if (dir == 0)
      {
        out << "IMPROPER LIBRARY REFERENCE '" << arg << "': "
            << strerror(errno) << endl;

        UTF8_ostream more;
        more << "path '" << path << "' could not be openend as directory: "
           << strerror(errno);
        Workspace::more_error() = UCS_string(more.get_data());
        return 0;
      }

   return dir;
}
//-----------------------------------------------------------------------------
bool
Command::is_directory(dirent * entry, const UTF8_string & path)
{
#ifdef _DIRENT_HAVE_D_TYPE
   return entry->d_type == DT_DIR;
#endif

UTF8_string filename = path;
UTF8_string entry_name(entry->d_name);
   filename.append('/');
   filename.append(entry_name);

DIR * dir = opendir(filename.c_str());
   if (dir) closedir(dir);
   return dir != 0;
}
//-----------------------------------------------------------------------------
void 
Command::lib_common(ostream & out, const UCS_string & arg, int variant)
{
   // 1. open directory
   //
UTF8_string path;
DIR * dir = open_LIB_dir(path, out, arg);
   if (dir == 0)   return;

   // 2. collect files and directories
   //
vector<UCS_string> apl_files;
vector<UCS_string> xml_files;
vector<UCS_string> directories;

   for (;;)
       {
         dirent * entry = readdir(dir);
         if (entry == 0)   break;   // directory done

         UCS_string filename(entry->d_name);
         if (is_directory(entry, path))
            {
              if (filename[0] == '.') continue;
              filename.append(UNI_ASCII_SLASH);
              directories.push_back(filename);
              continue;
            }

         const int dlen = strlen(entry->d_name);
         if (variant == 1)
            {
              if (filename[dlen - 1] != 'l')   continue;   // not .apl or .xml
              if (filename[dlen - 4] != '.')   continue;   // not .apl or .xml

              if (filename[dlen - 3] == 'a' && filename[dlen - 2] == 'p')
                {
                   filename.shrink(filename.size() - 4);   // skip extension
                   apl_files.push_back(filename);
                 }
              else if (filename[dlen - 3] == 'x' && filename[dlen - 2] == 'm')
                 {
                   filename.shrink(filename.size() - 4);   // skip extension
                   xml_files.push_back(filename);
                 }
            }
         else
            {
              if (filename[0] == '.')   continue;         // skip dot files ...
              if (filename[dlen - 1] == '~')   continue;  // and editor backups
              apl_files.push_back(filename);
            }
       }
   closedir(dir);

   // 3. sort directories and filenames alphabetically
   //
DynArray(const UCS_string *, directory_names, directories.size());
   loop(a, directories.size())   directory_names[a] = &directories[a];
   UCS_string::sort_names(directory_names, directories.size());

DynArray(const UCS_string *, apl_filenames, apl_files.size());
   loop(a, apl_files.size())   apl_filenames[a] = &apl_files[a];
   UCS_string::sort_names(apl_filenames, apl_files.size());

DynArray(const UCS_string *, xml_filenames, xml_files.size());
   loop(x, xml_files.size())   xml_filenames[x] = &xml_files[x];
   UCS_string::sort_names(xml_filenames, xml_files.size());

   // 4. list directories first, then files
   //
DynArray(const UCS_string *, filenames, apl_files.size() + xml_files.size()
                                        + directories.size()
         );

int count = 0;
   loop(dd, directories.size())
       filenames[count++] = directory_names[dd];

   for (int a = 0, x = 0;;)
       {
         if (a >= apl_files.size())   // end of apl_files reached
            {
              loop(xx, xml_files.size() - x)
                  filenames[count++] = xml_filenames[x + xx];
              break;
            }

         if (x >= xml_files.size())   // end of xml_files reached
            {
              loop(aa, apl_files.size() - a)
                  filenames[count++] = apl_filenames[a + aa];
              break;
            }

         // both APL and XML files. Compare them
         //
         const Comp_result comp = apl_filenames[a]->compare(*xml_filenames[x]);
         if (comp == COMP_LT)        // APL filename smaller
            {
              filenames[count++] = apl_filenames[a++];
            }
         else if (comp == COMP_GT)   // XML filename smaller
            {
              filenames[count++] = xml_filenames[x++];
            }
         else                        // same
            {
              ((UCS_string *)(apl_filenames[a]))->append(UNI_ASCII_PLUS);
              filenames[count++] = apl_filenames[a++];   ++x;
            }
       }
        
   // figure column widths
   //
   enum { tabsize = 4 };
vector<int> col_width;
   UCS_string::compute_column_width(col_width, filenames, count, tabsize,
                                    Workspace::get_PrintContext().get_PW());

   loop(c, count)
      {
        const int col = c % col_width.size();
        out << *filenames[c];
        if (col == (col_width.size() - 1) || c == (count - 1))
           {
             // last column or last item: print newline
             //
             out << endl;
           }
        else
           {
             // intermediate column: print spaces
             //
             const int len = tabsize*col_width[col] - filenames[c]->size();
             Assert(len > 0);
             loop(l, len)   out << " ";
           }
      }
}
//-----------------------------------------------------------------------------
void 
Command::cmd_LIB1(ostream & out, const UCS_string & arg)
{
   Command::lib_common(out, arg, 1);
}
//-----------------------------------------------------------------------------
void 
Command::cmd_LIB2(ostream & out, const UCS_string & arg)
{
   Command::lib_common(out, arg, 2);
}
//-----------------------------------------------------------------------------
void 
Command::cmd_LOG(ostream & out, const UCS_string & arg)
{
#ifdef DYNAMIC_LOG_WANTED

   log_control(arg);

#else

   out << "\n"
<< "Command ]LOG is not available, since dynamic logging was not\n"
"configured for this APL interpreter. To enable dynamic logging (which\n"
"will decrease performance), recompile the interpreter as follows:"

<< "\n\n"
"   ./configure DYNAMIC_LOG_WANTED=yes (... "
<< "other configure options"
<< ")\n"
"   make\n"
"   make install (or try: src/apl)\n"
"\n"

<< "above the src directory."
<< "\n";

#endif
}
//-----------------------------------------------------------------------------
void 
Command::cmd_MORE(ostream & out)
{
   if (Workspace::more_error().size() == 0)
      {
        out << "NO MORE ERROR INFO" << endl;
        return;
      }

   out << Workspace::more_error() << endl;
   return;
}
//-----------------------------------------------------------------------------
void 
Command::cmd_OFF(int exit_val)
{
   cleanup();
   COUT << endl;
   if (!uprefs.silent)   COUT << "Goodbye." << endl;
   exit(exit_val);
}
//-----------------------------------------------------------------------------
void
Command::cmd_OUT(ostream & out, vector<UCS_string> & args)
{
   if (args.size() == 0)
      {
        out << "BAD COMMAND" << endl;
        Workspace::more_error() =
                   UCS_string("missing filename in command )OUT");
        return;
      }

UCS_string fname = args.front();
   args.erase(args.begin());

UTF8_string filename = LibPaths::get_lib_filename(LIB_NONE, fname, false,
                                                  ".atf", 0);
   
FILE * atf = fopen(filename.c_str(), "w");
   if (atf == 0)
      {
        UTF8_string fname_utf8(fname);
        out << ")OUT " << fname << " failed: " << strerror(errno) << endl;
        char cc[200];
        snprintf(cc, sizeof(cc),
                 "command )OUT: could not open file %s for writing: %s",
                 fname_utf8.c_str(), strerror(errno));
        Workspace::more_error() = UCS_string(cc);
        return;
      }

uint64_t seq = 1;   // sequence number for records written
   Workspace::write_OUT(atf, seq, args);

   fclose(atf);
}
//-----------------------------------------------------------------------------
bool
Command::check_name_conflict(ostream & out, const UCS_string & cnew,
                             const UCS_string cold)
{
int len = cnew.size();
        if (len > cold.size())   len = cold.size();

   loop(l, len)
      {
        int c1 = cnew[l];
        int c2 = cold[l];
        if (c1 >= 'a' && c1 <= 'z')   c1 -= 0x20;   // uppercase
        if (c2 >= 'a' && c2 <= 'z')   c2 -= 0x20;   // uppercase
        if (l && (c1 != c2))   return false;   // OK: different
     }

   out << "BAD COMMAND" << endl;
   Workspace::more_error() = UCS_string(
          "conflict with existing command name in command ]USERCMD");

   return true;
}
//-----------------------------------------------------------------------------
bool
Command::check_redefinition(ostream & out, const UCS_string & cnew,
                            const UCS_string fnew, const int mnew)
{
   loop(u, user_commands.size())
     {
       const UCS_string cold = user_commands[u].prefix;
       const UCS_string fold = user_commands[u].apl_function;
       const int mold = user_commands[u].mode;

       if (cnew != cold)   continue;

       // user command name matches; so must mode and function
       if (mnew != mold || fnew != fold)
         {
           out << "BAD COMMAND" << endl;
           Workspace::more_error() 
             = UCS_string(
                          "conflict with existing user command definition"
                          " in command ]USERCMD");
         }
       return true;
     }

   return false;
}
//-----------------------------------------------------------------------------
void
Command::cmd_USERCMD(ostream & out, const UCS_string & cmd,
                     vector<UCS_string> & args)
{
   // ]USERCMD REMOVE-ALL
   // ]USERCMD REMOVE        ]existing-command
   // ]USERCMD ]new-command  APL-fun
   // ]USERCMD ]new-command  APL-fun  mode
   //
   if (args.size() < 2)
      {
        out << "BAD COMMAND" << endl;
        Workspace::more_error() =
                   UCS_string("too few parameters in command ]USERCMD");
        return;
      }

  if (args.size() == 1 && args[0].starts_iwith("REMOVE-ALL"))
     {
       user_commands.clear();
       out << "    All user-defined commands removed." << endl;
       return;
     }

  if (args.size() == 2 && args[0].starts_iwith("REMOVE"))
     {
       loop(u, user_commands.size())
           {
             if (user_commands[u].prefix.starts_iwith(args[1]) &&
                 args[1].starts_iwith(user_commands[u].prefix))   // same
                {
                  // print first and remove then!
                  //
                  out << "    User-defined command "
                      << user_commands[u].prefix << " removed." << endl;
                  user_commands.erase(user_commands.begin() + u);
                  return;
                }
           }

       out << "BAD COMMAND" << endl;
       Workspace::more_error() = UCS_string(
                "user command in command ]USERCMD REMOVE does not exist");
       return;
     }

   if (args.size() > 3)
      {
        out << "BAD COMMAND" << endl;
        Workspace::more_error() =
                   UCS_string("too many parameters in command ]USERCMD");
        return;
      }

const int mode = (args.size() == 3) ? args[2].atoi() : 0;
   if (mode < 0 || mode > 1)
      {
        out << "BAD COMMAND" << endl;
        Workspace::more_error() =
                   UCS_string("unsupported mode in command ]USERCMD");
        return;
      }

   // check command name
   //
   loop(c, args[0].size())
      {
        bool error = false;
        if (c == 0)   error = error || args[0][c] != ']';
        else          error = error || !Avec::is_symbol_char(args[0][c]);
        if (error)
           {
             out << "BAD COMMAND" << endl;
             Workspace::more_error() =
                   UCS_string("bad user command name in command ]USERCMD");
             return;
           }
      }

   // check conflicts with existing commands
   //
#define cmd_def(cmd_str, _cod, arg) \
   if (check_name_conflict(out, cmd_str, args[0]))   return;
#include "Command.def"
   if (check_redefinition(out, args[0], args[1], mode))
     {
       out << "    User-defined command "
           << args[0] << " installed." << endl;
       return;
     }

   // check APL function name
   //
   loop(c, args[1].size())
      {
        if (!Avec::is_symbol_char(args[1][c]))
           {
             out << "BAD COMMAND" << endl;
             Workspace::more_error() =
                   UCS_string("bad APL function name in command ]USERCMD");
             return;
           }
      }

user_command new_user_command = { args[0], args[1], mode };
   user_commands.push_back(new_user_command);

   out << "    User-defined command "
       << new_user_command.prefix << " installed." << endl;
}
//-----------------------------------------------------------------------------
void
Command::do_USERCMD(ostream & out, UCS_string & apl_cmd,
                    const UCS_string & line, const UCS_string & cmd,
                    vector<UCS_string> & args, int uidx)
{
  if (user_commands[uidx].mode > 0)   // dyadic
     {
        apl_cmd.append_quoted(cmd);
        apl_cmd.append(UNI_ASCII_SPACE);
        loop(a, args.size())
           {
             apl_cmd.append_quoted(args[a]);
             apl_cmd.append(UNI_ASCII_SPACE);
           }
     }

   apl_cmd.append(user_commands[uidx].apl_function);
   apl_cmd.append(UNI_ASCII_SPACE);
   apl_cmd.append_quoted(line);
}
//-----------------------------------------------------------------------------
#ifdef DYNAMIC_LOG_WANTED
void
Command::log_control(const UCS_string & arg)
{
   if (arg.size() == 0 || arg[0] == UNI_ASCII_QUESTION)  // no arg or '?'
      {
        for (LogId l = LID_MIN; l < LID_MAX; l = LogId(l + 1))
            {
              const char * info = Log_info(l);
              Assert(info);

              const bool val = Log_status(l);
              CERR << "    " << setw(2) << right << l << ": " 
                   << (val ? "(ON)  " : "(OFF) ") << left << info << endl;
            }

        return;
      }

LogId val = LogId(0);
bool skip_leading_ws = true;

   loop(a, arg.size())
      {
        if ((arg[a] <= ' ') && skip_leading_ws)   continue;
        skip_leading_ws = false;
        if (arg[a] < UNI_ASCII_0)   break;
        if (arg[a] > UNI_ASCII_9)   break;
        val = LogId(10*val + arg[a] - UNI_ASCII_0);
      }

   if (val >= LID_MIN && val <= LID_MAX)
      {
        const char * info = Log_info(val);
        Assert(info);
        const bool new_status = !Log_status(val);
        Log_control(val, new_status);
        CERR << "    Log facility '" << info << "' is now "
             << (new_status ? "ON " : "OFF") << endl;
      }
}
#endif
//-----------------------------------------------------------------------------
void
Command::transfer_context::process_record(const UTF8 * record, const
                                          vector<UCS_string> & objects)
{
const char rec_type = record[0];   // '*', ' ', or 'X'
const char sub_type = record[1];

   if (rec_type == '*')   // comment or similar
      {
        Log(LOG_command_IN)
           {
             const char * stype = " *** bad sub-record of *";
             switch(sub_type)
                {
                  case ' ': stype = " comment";     break;
                  case '(': {
                              stype = " timestamp";
                              YMDhmsu t(now());   // fallback if sscanf() != 7
                              if (7 == sscanf((const char *)(record + 1),
                                              "(%d %d %d %d %d %d %d)",
                                              &t.year, &t.month, &t.day,
                                              &t.hour, &t.minute, &t.second,
                                              &t.micro))
                                  {
                                    timestamp = t.get();
                                  }
                            }
                            break;
                  case 'I': stype = " imbed";       break;
                }

             CERR << "record #" << setw(3) << recnum << ": '" << rec_type
                  << "'" << stype << endl;
           }
      }
   else if (rec_type == ' ' || rec_type == 'X')   // object
      {
        if (new_record)
           {
             Log(LOG_command_IN)
                {
                  const char * stype = " *** bad sub-record of X";

//                          " -------------------------------------";
                  switch(sub_type)
                     {
                       case 'A': stype = " 2 ⎕TF array ";           break;
                       case 'C': stype = " 1 ⎕TF char array ";      break;
                       case 'F': stype = " 2 ⎕TF function ";        break;
                       case 'N': stype = " 1 ⎕TF numeric array ";   break;
                     }

                  CERR << "record #" << setw(3) << recnum
                       << ": " << stype << endl;
                }

             item_type = sub_type;
           }

        add(record + 1, 71);

        new_record = (rec_type == 'X');   // 'X' marks the final record
        if (new_record)
           {
             if      (item_type == 'A')   array_2TF(objects);
             else if (item_type == 'C')   chars_1TF(objects);
             else if (item_type == 'N')   numeric_1TF(objects);
             else if (item_type == 'F')   function_2TF(objects);
             else                         CERR << "????: " << data << endl;
             data.clear();
           }
      }
   else
      {
        CERR << "record #" << setw(3) << recnum << ": '" << rec_type << "'"
             << "*** bad record type '" << rec_type << endl;
      }
}
//-----------------------------------------------------------------------------
uint32_t
Command::transfer_context::get_nrs(UCS_string & name, Shape & shape) const
{
int idx = 1;

   // data + 1 is: NAME RK SHAPE RAVEL...
   //
   while (idx < data.size() && data[idx] != UNI_ASCII_SPACE)
         name.append(data[idx++]);
   ++idx;   // skip space after the name

int rank = 0;
   while (idx < data.size() &&
          data[idx] >= UNI_ASCII_0 &&
          data[idx] <= UNI_ASCII_9)
      {
        rank *= 10;
        rank += data[idx++] - UNI_ASCII_0;
      }
   ++idx;   // skip space after the rank

   loop (r, rank)
      {
        ShapeItem s = 0;
        while (idx < data.size() &&
               data[idx] >= UNI_ASCII_0 &&
               data[idx] <= UNI_ASCII_9)
           {
             s *= 10;
             s += data[idx++] - UNI_ASCII_0;
           }
        shape.add_shape_item(s);
        ++idx;   // skip space after shape[r]
      }
  
   return idx;
}
//-----------------------------------------------------------------------------
void
Command::transfer_context::numeric_1TF(const vector<UCS_string> & objects) const
{
UCS_string var_name;
Shape shape;
int idx = get_nrs(var_name, shape);

   if (objects.size() && !var_name.contained_in(objects))   return;

Symbol * sym = 0;
   if (Avec::is_quad(var_name[0]))   // system variable.
      {
        int len = 0;
        const Token t = Workspace::get_quad(var_name, len);
        if (t.get_ValueType() == TV_SYM)   sym = t.get_sym_ptr();
        else                               Assert(0 && "Bad system variable");
      }
   else                            // user defined variable
      {
        sym = Workspace::lookup_symbol(var_name);
        Assert(sym);
      }
   
   Log(LOG_command_IN)
      {
        CERR << endl << var_name << " rank " << shape.get_rank() << " IS '";
        loop(j, data.size() - idx)   CERR << data[idx + j];
        CERR << "'" << endl;
      }

Token_string tos;
   {
     UCS_string data1(data, idx, data.size() - idx);
     Tokenizer tokenizer(PM_EXECUTE, LOC);
     if (tokenizer.tokenize(data1, tos) != E_NO_ERROR)   return;
   }
 
   if (tos.size() != shape.get_volume())   return;

Value_P val(new Value(shape, LOC));
   new (&val->get_ravel(0)) IntCell(0);   // prototype

const ShapeItem ec = val->element_count();
   loop(e, ec)
      {
        const TokenTag tag = tos[e].get_tag();
        Cell * C = &val->get_ravel(e);
        if      (tag == TOK_INTEGER)  new (C) IntCell(tos[e].get_int_val());
        else if (tag == TOK_REAL)     new (C) FloatCell(tos[e].get_flt_val());
        else if (tag == TOK_COMPLEX)  new (C)
                                          ComplexCell(tos[e].get_cpx_real(),
                                                      tos[e].get_cpx_imag());
        else FIXME;
      }

   Assert(sym);
   sym->assign(val, LOC);
}
//-----------------------------------------------------------------------------
void
Command::transfer_context::chars_1TF(const vector<UCS_string> & objects) const
{
UCS_string var_name;
Shape shape;
int idx = get_nrs(var_name, shape);

   if (objects.size() && !var_name.contained_in(objects))   return;

Symbol * sym = 0;
   if (Avec::is_quad(var_name[0]))   // system variable.
      {
        int len = 0;
        const Token t = Workspace::get_quad(var_name, len);
        if (t.get_ValueType() == TV_SYM)   sym = t.get_sym_ptr();
        else                               Assert(0 && "Bad system variable");
      }
   else                            // user defined variable
      {
        sym = Workspace::lookup_symbol(var_name);
        Assert(sym);
      }
   
   Log(LOG_command_IN)
      {
        CERR << endl << var_name << " rank " << shape.get_rank() << " IS '";
        loop(j, data.size() - idx)   CERR << data[idx + j];
        CERR << "'" << endl;
      }

Value_P val(new Value(shape, LOC));
   new (&val->get_ravel(0)) CharCell(UNI_ASCII_SPACE);   // prototype

const ShapeItem ec = val->element_count();
   loop(e, ec)   new (&val->get_ravel(e)) CharCell(data[idx + e]);

   Assert(sym);
   sym->assign(val, LOC);
}
//-----------------------------------------------------------------------------
void
Command::transfer_context::array_2TF(const vector<UCS_string> & objects) const
{
   // an Array in 2 ⎕TF format
   //
UCS_string data1(data.get_items() + 1, data.size() - 1);
UCS_string var_or_fun;

   // data1 is: VARNAME←data...
   //
   if (objects.size())
      {
        UCS_string var_name;
        loop(d, data1.size())
           {
             const Unicode uni = data1[d];
             if (uni == UNI_LEFT_ARROW)   break;
             var_name.append(uni);
           }

        if (!var_name.contained_in(objects))   return;
      }

   var_or_fun = Quad_TF::tf2_inv(data1);

   Assert(var_or_fun.size());
}
//-----------------------------------------------------------------------------
void
Command::transfer_context::function_2TF(const vector<UCS_string> & objects)const
{
int idx = 1;
UCS_string fun_name;

   /// chars 1...' ' are the function name
   while ((idx < data.size()) && (data[idx] != UNI_ASCII_SPACE))
        fun_name.append(data[idx++]);
   ++idx;

   if (objects.size() && !fun_name.contained_in(objects))   return;

UCS_string statement;
   while (idx < data.size())   statement.append(data[idx++]);
   statement.append(UNI_ASCII_LF);

UCS_string fun_name1 = Quad_TF::tf2_inv(statement);
   if (fun_name1.size() == 0)   // tf2_inv() failed
      {
        CERR << "inverse 2 ⎕TF failed for the following APL statement: "
             << endl << "    " << statement << endl;
        return;
      }

Symbol * sym1 = Workspace::lookup_existing_symbol(fun_name1);
   Assert(sym1);
Function * fun1 = sym1->get_function();
   Assert(fun1);
   fun1->set_creation_time(timestamp);

   Log(LOG_command_IN)
      {
       const YMDhmsu ymdhmsu(timestamp);
       CERR << "FUNCTION '" << fun_name1 <<  "'" << endl
            << "   created: " << ymdhmsu.day << "." << ymdhmsu.month
            << "." << ymdhmsu.year << "  " << ymdhmsu.hour
            << ":" << ymdhmsu.minute << ":" << ymdhmsu.second
            << "." << ymdhmsu.micro << " (" << timestamp << ")" << endl;
      }
}
//-----------------------------------------------------------------------------
void
Command::transfer_context::add(const UTF8 * str, int len)
{

#if 0
   // helper to print the uni_to_cp_map when given a cp_to_uni_map.
   // The print-out can be triggered by )IN file.atf and then sorted
   // with the GNU/Linux command 'sort'.
   //
   loop(q, 128)
       fprintf(stderr, "  { 0x%4.4X, %u },\n", cp_to_uni_map[q], q + 128);
   exit(0);
#endif

const Unicode * cp_to_uni_map = Avec::IBM_quad_AV();
   loop(l, len)
      {
        const UTF8 utf = str[l];
        switch(utf)
           {
             case '^': data.append(UNI_AND);              break;   // ~ → ∼
             case '*': data.append(UNI_STAR_OPERATOR);    break;   // * → ⋆
             case '~': data.append(UNI_TILDE_OPERATOR);   break;   // ~ → ∼
             
             default:  data.append(Unicode(cp_to_uni_map[utf]));
           }
      }
}
//-----------------------------------------------------------------------------
bool
Command::parse_from_to(UCS_string & from, UCS_string & to,
                       const UCS_string & user_arg)
{
   // parse user_arg which is one of the following:
   //
   // 1.   (empty)
   // 2.   FROMTO
   // 3a.  FROM -
   // 3b.       - TO
   // 3c.  FROM - TO
   //
   from.clear();
   to.clear();

int s = 0;
bool got_minus = false;

   // skip spaces before from
   //
   while (s < user_arg.size() && user_arg[s] <= ' ') ++s;

   if (s == user_arg.size())   return false;   // case 1.: OK

   // copy left of - to from
   //
   while (s < user_arg.size()   &&
              user_arg[s] > ' ' &&
              user_arg[s] != '-')  from.append(user_arg[s++]);

   // skip spaces after from
   //
   while (s < user_arg.size() && user_arg[s] <= ' ') ++s;

   if (s < user_arg.size() && user_arg[s] == '-') { ++s;   got_minus = true; }

   // skip spaces before to
   //
   while (s < user_arg.size() && user_arg[s] <= ' ') ++s;

   // copy right of - to from
   //
   while (s < user_arg.size() && user_arg[s] > ' ')  to.append(user_arg[s++]);

   // skip spaces after to
   //
   while (s < user_arg.size() && user_arg[s] <= ' ') ++s;

   if (s < user_arg.size())   return true;   // error: non-blank after to

   if (!got_minus)   to = from;   // case 2.

   if (from.size() == 0 && to.size() == 0) return true;   // error: single -

   // "increment" TO so that we can compare ITEM < TO
   //
   if (to.size())   to.last() = (Unicode)(to.last() + 1);
   
   return false;   // OK
}
//-----------------------------------------------------------------------------
bool
Command::is_lib_ref(const UCS_string & lib)
{
   if (lib.size() == 1)   // single char: lib number
      {
        if (Avec::is_digit(lib[0]))   return true;
      }

   if (lib[0] == UNI_ASCII_FULLSTOP)   return true;

   loop(l, lib.size())
      {
        const Unicode uni = lib[l];
        if (uni == UNI_ASCII_SLASH)       return true;
        if (uni == UNI_ASCII_BACKSLASH)   return true;
      }

   return false;
}
//-----------------------------------------------------------------------------
