// smake -- simple make
//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Filename:      smake.cpp
// Creation Date: Fri Mar 29 21:43:39 PST 1996
// Last Modified: Fri May  8 16:33:29 PDT 2009 (updated c++ syntax for GCC 4)
// Syntax:        C++ 
// $Smake:        g++ -O -o %b %f -static -Llib -lmuseinfo %
//                && strip %b
//
// Description:   See ManPage() function.
//

#include "Array.h"
#include "Options.h"

#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#ifndef OLDCPP
   #include <iostream>
   #include <fstream>
   #include <sstream>
   #define SSTREAM stringstream
   #define CSTRING str().c_str()
   using namespace std;
#else
   #include <iostream.h>
   #include <fstream.h>
   #ifdef VISUAL
      #include <strstrea.h>
   #else
      #include <strstream.h>
   #endif
   #define SSTREAM strstream
   #define CSTRING str()
#endif

// command-line option variables:

int echoQ    = 0;                      // used with -e option
int quietQ   = 0;                      // used with -x option 
int numSkip  =4;                       // used with -n option
const char* commandmarker = "$Smake:"; // used with -c option
const char* versionSmake  = "";        // used with -v option

char MagicString[1024] = {0};
Array<Array<char> > passParams;
int    numPassParams;

char* mybasename;
char* filename;

// function declarations:
int   backwardPosition(char, char*);
void  find(const char*, istream&);
void  getOptions(void);
char  nextChar(istream&);
void  readCommand(SSTREAM&, const char*, istream&);
char* unsuffix(char*);
void  ManPage(void);
void  checkOptions(Options& opts);
int   usage(const char* command);

////////////////////////////////////////////////////////////////////////////////

int main(int argc, char* argv[]) {
   Options options(argc, argv);
   checkOptions(options);

   // find name of file to extract command from:
   filename = options.getArg(1);
   ifstream inFile(filename);
   if (!inFile) {
      cerr << "Cannot open file: " << filename << endl;
      exit(1);
   }
   mybasename = unsuffix(filename);

   // determine if any parameters are to be passed:
   numPassParams = options.getArgCount() - 1;
   passParams.setSize(numPassParams);
   int i, len;
   for (i=0; i<passParams.getSize(); i++) {
      len = strlen(options.getArg(i+2));
      passParams[i].setSize(len+1);
      strcpy(passParams[i].getBase(), options.getArg(i+2));
   }

   // create the commandstring and run it:
   SSTREAM strcommand;
   readCommand(strcommand, MagicString, inFile);
   if (strlen(strcommand.CSTRING) != 0) {
      if (!quietQ) {
         cout << strcommand.CSTRING << endl;
      }
      if (!echoQ) {
         system(strcommand.CSTRING);
      }
   } else {
      cerr << "No embedded command found in " << filename << endl;
   }
   

   if (mybasename != NULL)     delete [] mybasename;

   return 0;
}


////////////////////////////////////////////////////////////////////////////////


//////////////////////////////
//
// checkOptions --
//

void checkOptions(Options& opts) {
   opts.define("c|command-marker=s:$Smake:",  "set the default command marker");
   opts.define("e|echo=b",  "echo embedded command string without executing");
   opts.define("q|quiet=b", "execute command w/o printing command to terminal");
   opts.define("s|skip=i:4",    "skip characters on new line when continuing");
   opts.define("v|variant=s:",  "run a particular variant of $Smake command");
   opts.define("Z|author=b",    "print author of program and exit");
   opts.define("h|m|help|manpage=b", "print manpage and then exit");
   opts.process();

   if (opts.getArgCount() < 1) {
      usage(opts.getCommand());
      exit(1);
   }
   if (opts.getBoolean("author")) {
      cerr << "Written by Craig Stuart Sapp"
           <<  "<craig@ccrma.stanford.edu>; 1996\n";
      exit(1);
   }
   if (opts.getBoolean("manpage")) {
      ManPage();
      exit(1);
   }

   commandmarker = opts.getString("command-marker");
   versionSmake  = opts.getString("variant");
   echoQ         = opts.getBoolean("echo");
   quietQ        = opts.getBoolean("quiet");
   numSkip       = opts.getInteger("skip");

   strncpy(MagicString, commandmarker, 128);
   if (strlen(versionSmake) != 0) {
      int len = strlen(MagicString);
      if (MagicString[len-1] == ':') {
         MagicString[len-1] = '\0';
         strcat(MagicString, "-");
         strcat(MagicString, versionSmake);
         strcat(MagicString, ":");
      } else {
         strcat(MagicString, versionSmake);
      }
   }
}



//////////////////////////////
//
// usage
//

int usage(const char* command) {
   cerr << "\nUsage: " << command 
        << " [-s skip][-v variant |-c][-e|-x] input-file [parameters]\n"
        << "   Processes a file according the command string in the file.  "
        <<     "The command\n"
        << "   string starts with the pattern $Smake: and lasts until the "
        <<     "end of the line.\n"
        << "   Symbolic patterns in the command are:\n"
        << "      %f -- replaces with the filename string\n"
        << "      %b -- replaces with the filename base (removes all "
        <<      "characters after\n            last \".\""
        << " of file name and also removes the dot.\n"
        << "Type " << command << " -h for the man page.\n"
        << endl;
   return 0;
}



//////////////////////////////
//
// backwardPosition -- return the position of the last sought
//	character in the string.
//

int backwardPosition(char sought, char *string) {
   int i;
   for (i = strlen(string) - 1; i>=0; i--) {
      if (sought == string[i]) {
         break;
      }
   }
   return i;
}



//////////////////////////////
//
// unsuffix -- remove the .* extension of a filename.
//

char* unsuffix(char *string) {
   int dotPosition;
   char *base;

   dotPosition = backwardPosition('.', string);

   if (dotPosition != -1) {
      base = new char[dotPosition+1];
      if (base == NULL) {
         cerr << "Out of memory." << endl;
         exit(1);
      }
      strcpy(base, string);
      base[dotPosition] = '\0';
   } else {
      base = new char[1];
      base[0] = '\0';
   }
   return base;
}



//////////////////////////////
//
// find -- find a string in an istream, and position the istream after
//	that string or at the end of the file.
//

void find(const char* searchString, istream& inFile) {
   char c;
   int length = strlen(searchString);
   int position = 0;

   while (position < length && inFile.get(c)) {
      if (c == searchString[position]) 
         position++;
      else 
         position = 0;
   }
}



//////////////////////////////
//
// nextChar -- return the next non-space, non-return character in the
// 	(text) file.  return '\0' if end-of-file or a return character.
//

char nextChar(istream& inFile) {
   char c = '\0';

   while (inFile.get(c) && isspace(c)) {
      if (c == '\n') {
         c = '\0';
         return c;
      }
   }
   return c;
}



//////////////////////////////
//
// readCommand -- read the command string from the file.
//

void readCommand( SSTREAM& strcommand, const char* commandMarker, 
      istream& inFile) {
   char c;

   find(commandMarker, inFile);

   c = nextChar(inFile);
   if (c == '\0') return;
   else strcommand << c;

   while (inFile.get(c) && c != '\n') {
      if (c == '%') {
         inFile.get(c);
         switch (c) {
            case '\n': 
               for (int i=0; i<numSkip; i++) {
                  inFile.get(c);
                  if (c == '\n') {
                     cerr << "Error: too many spaces to skip on line"
                          << endl;
                     exit(1);
                  }
               }
               while (inFile.get(c) && isspace(c)) {
                  if (c == '\n') return;
               }
               strcommand << c;
               break;         
            case '1': case '2': case '3': case '4': case '5': 
            case '6': case '7': case '8': case '9':
               if (numPassParams > (int)c - '0' - 1) {
                  strcommand << passParams[(int)c - '0' - 1].getBase();
               }
               else {
                  strcommand << '%' << c;
               }
               break;
            case 'f': 
               strcommand << filename;
               break;
            case 'b': 
               strcommand << mybasename;
               break;
            default:
               strcommand << '%' << c;      
         }
      } else {
         strcommand << c;
      }
   }
}



//////////////////////////////
//
// ManPage
//

void ManPage(void) {
   cout << 
"\n"
"SMAKE(1)            UNIX Programmer's Manual             SMAKE(1)\n"
"\n"
"\n"
"\n"
"NAME\n"
"     smake - Simple MAKE.  Executes a command string embedded in\n"
"     a file.\n"
"\n"
"SYNOPSIS\n"
"     smake [-s skip][-v variant |-c][-e|-x] input-file [parame-\n"
"           ters]\n"
"\n"
"DESCRIPTION\n"
"     Smake searches for the character pattern $Smake: in a file.\n"
"     When it finds that pattern, smake will execute, as a shell\n"
"     command, the text following $Smake: to the end of the line.\n"
"\n"
"     There are several symbolic patterns that can be used in the\n"
"     embedded command:\n"
"\n"
"     %f       If this character pattern is in the embedded com-\n"
"              mand, the input filename passed to smake will be\n"
"              substituted.\n"
"\n"
"     %F       If this character pattern is in the embedded com-\n"
"              mand, the input filename passed to smake will be\n"
"              substituted, removing any directory location.\n"
"\n"
"     %b       If this character pattern is in the command, the\n"
"              filename minus the characters after the last period\n"
"              will be removed, as well as the last period.  For\n"
"              example, the basename of dir/test.c is dir/test .\n"
"\n"
"     %B       If this character pattern is in the command, the\n"
"              filename minus the characters after the last period\n"
"              will be removed, as well as the last period.  For\n"
"              example, the Basename of dir/test.c is test .\n"
"\n"
"     %<newl>  Where <newl> is the newline character. If % comes\n"
"              at the end of the line with no other characters\n"
"              except the newline character following it, the\n"
"              embedded command continues on the following line.\n"
"              The -s option determines where the command resumes\n"
"              on the next line.\n"
"\n"
"     %1--%9   If any parameters follow the input-file on the com-\n"
"              mand line, then these parameters get matched to the\n"
"              symbols %1, %2, etc. up to %9.  If there is no\n"
"              passed parameter for a particular symbol, then it\n"
"              is left unchanged in the command.\n"
"\n"
"OPTIONS\n"
"     -s skip  skip is the number of characters to skip at the\n"
"              beginning of a new line for the shell command\n"
"              line-continuation.  The default value of skip is 4.\n"
"              It is an error to skip beyond the end of the line.\n"
"\n"
"     -v variant\n"
"              variant is a string used to distinguish between\n"
"              different possible commands to extract.  The form\n"
"              of the marker in the file is $Smake-variant:.\n"
"\n"
"     -e       echo the embedded command string without executing\n"
"              it.\n"
"\n"
"     -x       execute the command string only, without printing\n"
"              it to the terminal as well.\n"
"\n"
"     -c pattern\n"
"              pattern is a string to replace the default marker\n"
"              $Smake: with pattern.\n"
"\n"
"EXAMPLES\n"
"     A command to compile a c program, and strip the executable:\n"
"\n"
"     // $Smake:    cc -O -o %b %f && strip %b\n"
"\n"
"     A command to produce and display a PostScript file from a\n"
"     TeX file:\n"
"\n"
"     % $Smake:     tex %b && dvips -o %b.ps %b && open %b.ps\n"
"\n"
"FILES\n"
"     /user/c/craig/lang/c/smake.cc the source code\n"
"\n"
"BUGS\n"
"     Cannot use the -v option and the -c option at the same time.\n"
   << endl;

}


// md5sum: f27722754f53bfe620cad5b9d2f3b17d smake.cpp [20090511]