4.4. Una aplicación con cadenas

Si ha observado atentamente los códigos de ejemplo de este libro, habrá observado que ciertos elementos en los comentarios envuelven el código. Son usados por un programa en Python que escribió Bruce para extraer el código en ficheros y configurar makefiles para construir el código. Por ejemplo, una doble barra segida de dos puntos en el comienzo de una línea denota la primera línea de un fichero de código . El resto de la línea contiene información describiendo el nombre del fichero y su locaización y cuando deberia ser solo compilado en vez constituir un fichero ejecutable. Por ejemplo, la primera línea del programa anterior contiene la cadena C03:IWCompare.cpp, indicando que el fichero IWCompare.cpp deberia ser extraido en el directorio C03.

La última línea del fichero fuente contiene una triple barra seguida de dos puntos y un signo "~". Es la primera línea tiene una exclamación inmediatamente después de los dos puntos, la primera y la última línea del código fuente no son para ser extraídas en un fichero (solo es para ficheros solo de datos). (Si se está preguntando por que evitamos mostrar estos elementos, es por que no queremos romper el extractor de código cuando lo aplicamos al texto del libro!).

El programa en Python de Bruce hace muchas más cosas que simplemente extraer el código. Si el elemento "{O}" sigue al nombre del fichero, su entrada en el makefile solo será configurada para compilar y no para enlazarla en un ejecutable. (El Test Framework en el Capítulo 2 está contruida de esta manera). Para enlazar un fichero con otro fuente de ejemplo, el fichero fuente del ejecutable objetivo contendrá una directiva "{L}", como aquí:

//{L} ../TestSuite/Test

Esta sección le presentará un programa para extraer todo el código para que pueda compilarlo e inspeccionarlo manualmente. Puede usar este programa para extraer todo el codigo de este libro salvando el fichero como un fichero de texto[9] (llamémosle TICV2.txt)y ejecutando algo como la siguiente línea de comandos: C:> extractCode TICV2.txt /TheCode

Este comando lee el fichero de texto TICV2.txt y escribe todos los archivos de código fuente en subdirectorios bajo el definido /TheCode. El arbol de directorios se mostrará como sigue:

TheCode/ C0B/ C01/ C02/ C03/ C04/ C05/ C06/ C07/ C08/ C09/ C10/ C11/ TestSuite/

Los ficheros de código fuente que contienen los ejemplos de cada capítulo estarán en el correspondiente directorio.

Aquí está el programa:

//: C03:ExtractCode.cpp {-edg} {RunByHand}
// Extracts code from text.
#include <cassert>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
using namespace std;

// Legacy non-standard C header for mkdir()
#if defined(__GNUC__) || defined(__MWERKS__)
#include <sys/stat.h>
#elif defined(__BORLANDC__) || defined(_MSC_VER) \
  || defined(__DMC__)
#include <direct.h>
#else
#error Compiler not supported
#endif

// Check to see if directory exists
// by attempting to open a new file
// for output within it.
bool exists(string fname) {
  size_t len = fname.length();
  if(fname[len-1] != '/' && fname[len-1] != '\\')
    fname.append("/");
  fname.append("000.tmp");
  ofstream outf(fname.c_str());
  bool existFlag = outf;
  if(outf) {
    outf.close();
    remove(fname.c_str());
  }
  return existFlag;
}

int main(int argc, char* argv[]) {
  // See if input file name provided
  if(argc == 1) {
    cerr << "usage: extractCode file [dir]" << endl;
    exit(EXIT_FAILURE);
  }
  // See if input file exists
  ifstream inf(argv[1]);
  if(!inf) {
    cerr << "error opening file: " << argv[1] << endl;
    exit(EXIT_FAILURE);
  }
  // Check for optional output directory
  string root("./");  // current is default
  if(argc == 3) {
    // See if output directory exists
    root = argv[2];
    if(!exists(root)) {
      cerr << "no such directory: " << root << endl;
      exit(EXIT_FAILURE);
    }
    size_t rootLen = root.length();
    if(root[rootLen-1] != '/' && root[rootLen-1] != '\\')
      root.append("/");
  }
  // Read input file line by line
  // checking for code delimiters
  string line;
  bool inCode = false;
  bool printDelims = true;
  ofstream outf;
  while(getline(inf, line)) {
    size_t findDelim = line.find("//" "/:~");
    if(findDelim != string::npos) {
      // Output last line and close file
      if(!inCode) {
        cerr << "Lines out of order" << endl;
        exit(EXIT_FAILURE);
      }
      assert(outf);
      if(printDelims)
        outf << line << endl;
      outf.close();
      inCode = false;
      printDelims = true;
    } else {
      findDelim = line.find("//" ":");
      if(findDelim == 0) {
        // Check for '!' directive
        if(line[3] == '!') {
          printDelims = false;
          ++findDelim;  // To skip '!' for next search
        }
        // Extract subdirectory name, if any
        size_t startOfSubdir =
          line.find_first_not_of(" \t", findDelim+3);
        findDelim = line.find(':', startOfSubdir);
        if(findDelim == string::npos) {
          cerr << "missing filename information\n" << endl;
          exit(EXIT_FAILURE);
        }
        string subdir;
        if(findDelim > startOfSubdir)
          subdir = line.substr(startOfSubdir,
                               findDelim - startOfSubdir);
        // Extract file name (better be one!)
        size_t startOfFile = findDelim + 1;
        size_t endOfFile =
          line.find_first_of(" \t", startOfFile);
        if(endOfFile == startOfFile) {
          cerr << "missing filename" << endl;
          exit(EXIT_FAILURE);
        }
        // We have all the pieces; build fullPath name
        string fullPath(root);
        if(subdir.length() > 0)
          fullPath.append(subdir).append("/");
        assert(fullPath[fullPath.length()-1] == '/');
        if(!exists(fullPath))
#if defined(__GNUC__) || defined(__MWERKS__)
          mkdir(fullPath.c_str(), 0);  // Create subdir
#else
          mkdir(fullPath.c_str());  // Create subdir
#endif
        fullPath.append(line.substr(startOfFile,
                        endOfFile - startOfFile));
        outf.open(fullPath.c_str());
        if(!outf) {
          cerr << "error opening " << fullPath
               << " for output" << endl;
          exit(EXIT_FAILURE);
        }
        inCode = true;
        cout << "Processing " << fullPath << endl;
        if(printDelims)
          outf << line << endl;
      }
      else if(inCode) {
        assert(outf);
        outf << line << endl;  // Output middle code line
      }
    }
  }
  exit(EXIT_SUCCESS);
} ///:~

Listado 4.33. C03/ExtractCode.cpp


Primero observará algunas directivas de compilación condicionales. La función mkdir(), que crea un directorio en el sistema de ficheros, se define por el estándar POSIX[10] en la cabecera (<direct.h>). La respectiva signatura de mkdir() también difiere: POSIX especifica dos argumentos, las viejas versiones sólo uno. Por esta razón, existe más de una directiva de compilación condicional después en el programa para elegir la llamada correcta a mkdir(). Normalmente no usamos compilaciones condicionales en los ejemplos de este libro, pero en este programa en particular es demasiado útil para no poner un poco de trabajo extra dentro, ya que puede usarse para extraer todo el código con él.

La función exists() en ExtractCode.cpp prueba que un directorio existe abriendo un fiechero temporal en él. Si la obertura falla, el directorio no existe. Borre el fichero enviando su nombre como unchar* a std::remove().

El programa principal valida los argumentos de la línea de comandos y después lee el fichero de entrada línea por línea, mirando por los delimitadores especiales de código fuente. La bandera booleana inCode indica que el programa esta en el medio de un fichero fuente, así que las lineas deben ser extraídas. La bandera printDelims será verdadero si el elemento de obertura no está seguido de un signo de exclamanción; si no la primera y la última línea no son escritas. Es importante comprobar el último delimitador primero, por que el elemnto inicial es un subconjuntom y buscando por el elemento inicial debería retornar cierto en ambos casos. Si encontramos el elemento final, verificamos que estamos en el medio del procesamiento de un fichero fuente; sino, algo va mal con la manera en que los delimitadores han sido colocados en el fichero de texto. Si inCode es verdadero, todo está bien, y escribiremos (opcionalmente) la última linea y cerraremos el fichero. Cuando el elemento de obertura se encuentra, procesamos el directorio y el nombre del fichero y abrimos el fichero. Las siguientes funciones relacionadas con string fueron usadas en este ejemplo: length( ), append( ), getline( ), find( ) (dos versiones), find_first_not_of( ), substr( ),find_first_of( ), c_str( ), y, por supuesto, operator<<( )



[9] Esté alerta porque algunas versiones de Microsoft Word que substituyen erroneamente los caracteres con comilla simple con un carácter ASCII cuando salva el documento como texto, causan un error de compilación. No tenemos idea de porqué pasa esto. Simplemente reemplace el carácter manualmente con un apóstrofe.

[10] POSIX, un estándar IEEE, es un "Portable Operating System Interface" (Interficie de Sistema Operativo Portable) y es una generalización de muchas de las llamadas a sistema de bajo nivel encontradas en los sistemas UNIX.