Lavorare con i file batch

L'interprete dei comandi (COMMAND.COM nella configurazione DOS standard) fornisce una interfaccia per l'esecuzione dei programmi (a volte definiti comandi esterni [1]) e rende disponibili alcuni comandi interni, così detti in quanto implementati mediante routine interne all'interprete stesso (COPY, DEL, DIR, etc.), nonché la capacità, utilizzabile dalle applicazioni opportunamente progettate, di effettuare redirezioni.

L'esecuzione di sequenze di comandi interni ed esterni può essere automatizzata mediante i file batch, che l'interprete è in grado di leggere ed eseguire riga per riga: ognuna contiene, in formato ASCII, un singolo comando; sono inoltre disponibili istruzioni per il controllo del flusso elaborativo, quali FOR, IF (utilizzabili direttamente da prompt) e GOTO. Per i dettagli circa la sintassi dei comandi interni si rimanda alla manualistica DOS; in questa sede si vuole sottolineare che, per mezzo di questi soltanto, è spesso difficile (se non impossibile) implementare algoritmi di una certa complessità. Ad esempio, la IF consente di controllare il contenuto del registro ERRORLEVEL o di verificare l'esistenza di un file[2]:

IF EXIST PIPPO GOTO TROVATO
ECHO PIPPO NON C'E'
GOTO END
:TROVATO
....
:END

Non è tuttavia possibile effettuare test su alcuna caratteristica del file stesso, quali data, ora, dimensione, né accertare se si tratti piuttosto di una directory.

L'elenco dei principali limiti all'efficacia dei file batch può continuare a lungo: non vi è modo di inserire in comandi batch stringhe contenenti data e ora di elaborazione, o parti di esse; non è possibile ritardare l'esecuzione di un comando ad un'ora specificata; il comando COPY fallisce se il file origine ha dimensione 0 byte; non vi sono strumenti in grado di estrarre da un flusso ASCII parti di testo in modo "mirato", ad eccezione del programma FIND, che ne visualizza le righe contenenti (o no) una data stringa; l'output di un comando non può essere utilizzato come parte di un successivo comando; una modifica alla lista degli argomenti del comando FOR richiede che sia modificato il file batch contenente il comando stesso[3].

Dette limitazioni sono superate con una minima interattività da parte dell'utilizzatore, ma possono originare problemi quasi insormontabili laddove vi sia la necessità di una completa automazione (ad esempio in elaborazioni notturne): il presente paragrafo presenta alcuni programmi volti a superare le carenze cui si è fatto cenno[4]. Lo scopo è fornire un insieme di spunti e idee perfettibili e, al tempo stesso, adattabili a piacere secondo le specifiche esigenze di ciascuno.

L'idea più semplice: EMPTYLVL

La semplicità del listato che segue è assolutamente disarmante: il programma legge lo standard input ed immediatamente termina, valorizzando il registro ERRORLEVEL a 1 se lo stream stdin è vuoto o contiene solo spazi, tabulazioni o caratteri di ritorno a capo; in caso contrario ERRORLEVEL è azzerato.

/******************************************************************************

    EMPTYLVL.C - 20/01/93 - Barninga_Z!

    Legge una stringa da STDIN e setta il registro errorlevel a 1 se la
    stringa contiene solo spazi e/o tabs e/o CR e/o LF. In caso contrario
    il registro e' posto a 0.

    Compilato sotto Borland C++ 3.01:

    bcc -O -rd emptylvl.c

******************************************************************************/

#include <stdio.h>
#include <string.h>

#define  MAXHEAP     4096
#define  MAXBUF      2048
#define  NULLCHARS   " \t\n\r"

extern unsigned _heaplen = MAXHEAP;

int main(void)
{
    char buffer[MAXBUF];

    *buffer = NULL;
    gets(buffer);
    if(!strtok(buffer,NULLCHARS))
        return(1);                         // stringa nulla: errorlevel = 1
    return(0);                             // stringa non nulla: errorlevel = 0
}

Come si vede, l'unica particolarità tecnica di qualche rilevanza è costituita dall'assegnazione di un valore predefinito alla variabile _heaplen, per fissare la massima dimensione dello heap e limitare così al minimo indispensabile la quantità di memoria necessaria per il caricamento e l'esecuzione del programma.

EMPTYLVL si rivela utile in molte situazioni: ad esempio ci consente di scoprire se un file è vuoto, o se si tratta di una directory. Vediamo un esempio:

IF EXIST PIPPO GOTO TROVATO
ECHO PIPPO NON C'E'
GOTO END
:TROVATO
DIR | FIND "PIPPO" | FIND "<DIR>" | EMPTYLVL
IF ERRORLEVEL 1 GOTO PIPPOFIL
ECHO PIPPO E' UNA DIRECTORY
GOTO END
:PIPPOFIL
DIR | FIND "PIPPO" | FIND " 0 " | EMPTYLVL
IF ERRORLEVEL 1 GOTO PIPPODAT
ECHO IL FILE PIPPO E' VUOTO
GOTO END
:PIPPODAT
TYPE PIPPO | MORE
:END

Le prime righe del batch non rappresentano una novità rispetto quanto sopra accennato. Al contrario, la riga

DIR | FIND "PIPPO" | FIND "<DIR>" | EMPTYLVL

merita qualche commento. Il comando DIR produce l'elenco di tutti i file e subdirectory presenti nella directory di default: il simbolo '|' ne effettua il piping [5] al primo comando FIND (esterno), che lo scandisce e scrive, a sua volta, sullo standard output solo le righe contenenti la stringa "PIPPO". Ma l'output del primo FIND è trasformato (ancora mediante piping) in standard input per il secondo comando FIND, che scrive sul proprio standard output solo le righe contenenti la stringa "<DIR>": ne segue che se il file PIPPO è, in realtà, una directory, lo standard input di EMPTYLVL contiene la riga ad essa relativa dell'output originario di DIR; se invece PIPPO è un file, lo standard input di EMPTYLVL risulta vuoto. Nel primo caso, pertanto, ERRORLEVEL è posto a 0 e la IF non effettua il salto: viene visualizzata la stringa "PIPPO E' UNA DIRECTORY". Nel secondo caso ERRORLEVEL vale 1 e l'esecuzione salta all'etichetta :PIPPOFIL, a partire dalla quale, con un algoritmo del tutto analogo a quello testè commentato, si verifica se la dimensione di PIPPO riportata da DIR è 0. Se il file non è vuoto, il suo contenuto viene visualizzato, una schermata alla volta, grazie all'azione combinata del comandi TYPE (interno) e MORE (esterno).

Data e ora nei comandi: DATECMD

Il programma DATECMD è concepito per consentire l'inserimento automatico di data e ora, o parti di esse, nei comandi DOS. Esso scandisce la propria command line alla ricerca di sequenze note di simboli, che definiamo, per comodità, macro, e le sostituisce con la parte di data o ora che rappresentano, valorizzata in base a data e ora di sistema. Ogni macro è costituita dal carattere '@', seguito da una lettera che identifica il valore di sostituzione: così, ad esempio, @M indica il giorno del mese, espresso con due cifre. Le macro ammesse ed il loro significato sono elencati di seguito.

@@il carattere at (@); utile per inserire una '@' nella command line
@'le virgolette ("); utile per inserire le virgolette nella command line
@Al'anno; espresso con quattro cifre (es.: 1994)
@al'anno; espresso con due sole cifre (es.: 94)
@Mil mese; espresso con due cifre (es.: 07)
@mil mese; espresso con una sola cifra se la prima è 0 (es.: 7)
@Gil giorno del mese; espresso con due cifre (es.: 05)
@gil giorno del mese; espresso con una sola cifra se la prima è 0 (es.: 5)
@Ril giorno dell'anno; espresso con tre cifre (es.: 084)
@ril giorno dell'anno; espresso con una o due cifre se le prime sono 0 (es.: 84)
@Ol'ora; espressa con due cifre (es.: 02)
@ol'ora; espressa con una sola cifra se la prima è 0 (es.: 2)
@Iil minuto; espresso con due cifre (es.: 06)
@iil minuto; espresso con una sola cifra se la prima è 0 (es.: 6)
@Sil secondo; espresso con due cifre (es.: 01)
@sil secondo; espresso con una sola cifra se la prima è 0 (es.: 1)
@Eil giorno della settimana; è indicato il nome intero (es.: Lunedì)
@eil giorno della settimana; indicato mediante i primi tre caratteri del nome (es.: Lun)
@Wil giorno della settimana; espresso con un numero da 0 (Domenica) a 6 (Sabato)

Vediamo un esempio pratico: se il comando

datecmd echo Sono le @O:@I:@S di @E @g/@M/@a, @r^ giorno dell'anno: @'Ciao, @@!'@

viene eseguito alle 14:52:20 del 2 novembre 1994, DATECMD esegue in realtà il comando

echo Sono le 14:52:20 di Mercoledì 2/11/94, 306^ giorno dell'anno: "Ciao, @!"

DATECMD accetta inoltre due opzioni­d­v, che devono precedere la command line da eseguire. La prima consente di specificare uno "slittamento" di data, positivo o negativo. Se nel comando dell'esempio precedente si antepone ­v­1 alla command line di DATECMD (cioè a echo), l'ouput prodotto diventa

echo Sono le 14:52:20 di Martedì 1/11/94, 305^ giorno dell'anno: "Ciao, @!"

L'opzione ­v, invece, richiede a DATECMD di visualizzare soltanto, ma non eseguire, la command line risultante a seguito della risoluzione delle macro.

Segue il listato, ampiamente commentato, del programma (PARSEOPT.OBJ e le funzioni di manipolazione delle date sono trattati a parte).

/*-----------------------------------------------------------------------------

    DATECMD.C - Barninga_Z! - 27/04/1993

    Esegue command lines DOS con la possibilita' di parametrizzarle rispetto
    alla data e all'ora: nella command line, digitata dopo il nome del
    programma (DATECMD), possono essere inserite delle macro, costituite dal
    carattere macro (@) e da uno dei caratteri elencati nel sorgente (nella
    definizione dell'array delle macro). Dette macro vengono espanse nella
    stringa con il corrispondente significato. In ogni command line possono
    comparire piu' macro, ed ogni macro può comparire piu' volte. La macro
    @' viene espansa nelle virgolette ("); la macro @@ viene espansa nella
    atsign (@). L'utilita' di queste macro e' evidente nel caso in cui si
    debbano inserire le virgolette nella command line o nel caso in cui una
    sequenza di caratteri che non deve essere espansa comprenda in casualmente
    i caratteri costituienti una macro. Ad esempio, @M viene espanso nel mese
    corrente; per non espanderlo bisogna digitare @@M (@@ viene espanso in
    @ e la M non viene modificata). Se la command line e' preceduta dalla
    opzione -v essa viene solo visualizzata e non eseguita. Se è preceduta
    da -d si possono specificare i giorni di differenza rispetto alla data
    attuale (+|- gg).
    _stklen e _heaplen sono settate per ridurre l'ingombro in memoria del
    programma, visto che la command line e' eseguita con una system(). Per
    questo motivo, inoltre, l'interprete dei comandi deve essere accessibile.

    Compilato sotto Borland C++ 3.1:

    bcc -k- -O -d datecmd.c parseopt.obj date2jul.obj jul2date.obj isleapyr.obj

-----------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <process.h>
#include <dir.h>
#include <errno.h>
#include <ctype.h>

#include "parseopt.h"        // per la gestione delle opzioni di command line

#define  _PRG_               "DATECMD"     // nome del programma
#define  _VERSION_           "1.5"         // versione
#define  _YEAR_              "94"          // anno di rilascio
#define  MAXCMD              128           // max lungh. cmd line (ENTER incl.)
#define  MACRO_FLAG          '@'           // carattere che segnala la macro
#define  DQUOTE_CHR          '\x27'        // sostituisce le virgolette
#define  SWITCH              '-'           // switch character per opzioni
#define  YEARLEN             365           // giorni dell'anno non bisestile
#define  WEEKLEN             7             // giorni della settimana
#define  WDAYSHORTLEN        3             // lung. stringa breve giorno sett.

//-----------------------------------------------------------------------------
// prototipi delle funzioni del corpo del programma
//-----------------------------------------------------------------------------

long date2jul(int day,int month,int year);
int isleapyear(int year);
int jul2date(long jul,int *day,int *month,int *year);

int  main(int argc,char **argv);

void  adjustOrdinal(int baseYear,int shift);
void  adjustWeekDay(int len);
void  fatalError(int errNum);
void  help(void);
char *initProg(int argc,char **argv);
int   isDigitStr(char *string);
char *parseCmd(char *cmdArgs);

//-----------------------------------------------------------------------------
// Ottimizzazione dell'uso della memoria
//-----------------------------------------------------------------------------

extern unsigned _heaplen = 4096;
extern unsigned _stklen  = 2048;

//-----------------------------------------------------------------------------
// Messaggi di errore e di aiuto
//-----------------------------------------------------------------------------

#define  _E_BADARGS          0
#define  _E_CMDLONG          1
#define  _E_BADOPTIONS       2
#define  _E_ALLOCMEM         3

char *weekdays[] = {
    "Domenica",
    "Lunedì",
    "Martedì",
    "Mercoledì",
    "Giovedì",
    "Venerdì",
    "Sabato",
};

char *errors[] = {
    "E00: Numero di argomenti errato",
    "E01: La Command Line risultante è troppo lunga",
    "E02: Opzioni errate",
    "E03: Memoria insufficiente",
};

char *helptext = "\
Uso: DATECMD [-v][-d+|-g] CmdLine\n\
CmdLine è la command line da eseguire (-v visualizza soltanto, -d modifica la\n\
data di +/- g giorni); può contenere una o più delle seguenti macro:\n\
";

//-----------------------------------------------------------------------------
// Variabili globali per inizializzazione e altri scopi
//-----------------------------------------------------------------------------

struct tm *tData;                  // contiene data e ora correnti allo startup

char tStr[20];                     // buffer per sprintf() temporanee

//-----------------------------------------------------------------------------
// gruppo delle funzioni per il macro processing. Ciascuna di esse viene
// invocata se e' incontrata la macro corrispondente (vedere l'array macros)
// e restituisce un puntatore a stringa, che deve essere passato a strcpy()
// per copiare l'espansione della macro nella stringa di comando che sara'
// passata a spawn()
//-----------------------------------------------------------------------------

char *retMacroFlag(void)               // restituisce il carattere MACRO_FLAG
{
    extern char tStr[];

    sprintf(tStr,"%c",MACRO_FLAG);
    return(tStr);
}

char *retDoubleQuote(void)             // restituisce il carattere '"'
{
    sprintf(tStr,"%c",'\x22');
    return(tStr);
}

char *getYear(void)                    // anno, 4 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d%02d",tData->tm_year < 80 ? 20 : 19,tData->tm_year);
    return(tStr);
}

char *getYear1(void)                   // anno, 2 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d",tData->tm_year);
    return(tStr);
}

char *getMonth(void)                   // mese, 2 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d",tData->tm_mon);
    return(tStr);
}

char *getMonth1(void)                  // mese, 1 o 2 cifre
{
    extern char tStr[];

    getMonth();
    if(*tStr == '0')
        return(tStr+1);
    return(tStr);
}

char *getDay(void)                     // giorno del mese, 2 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d",tData->tm_mday);
    return(tStr);
}

char *getDay1(void)                    // giorno del mese, 1 o 2 cifre
{
    extern char tStr[];

    getDay();
    if(*tStr == '0')
        return(tStr+1);
    return(tStr);
}

char *getOrdinal(void)                 // giorno dell'anno, 3 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%03d",tData->tm_yday);
    return(tStr);
}

char *getOrdinal1(void)                // giorno dell'anno, 1 o 2 o 3 cifre
{
    register i;
    extern char tStr[];

    getOrdinal();
    for(i = 0; tStr[i] == '0'; )
        i++;
    return(tStr+i);
}

char *getHour(void)                    // ora, 2 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d",tData->tm_hour);
    return(tStr);
}

char *getHour1(void)                   // ora, 1 o 2 cifre
{
    extern char tStr[];

    getHour();
    if(*tStr == '0')
        return(tStr+1);
    return(tStr);
}

char *getMin(void)                     // minuto, 2 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d",tData->tm_min);
    return(tStr);
}

char *getMin1(void)                    // minuto, 1 o 2 cifre
{
    extern char tStr[];

    getMin();
    if(*tStr == '0')
        return(tStr+1);
    return(tStr);
}

char *getSec(void)                     // secondo, 2 cifre
{
    extern struct tm *tData;
    extern char tStr[];

    sprintf(tStr,"%02d",tData->tm_sec);
    return(tStr);
}

char *getSec1(void)                    // secondo, 1 o 2 cifre
{
    extern char tStr[];

    getSec();
    if(*tStr == '0')
        return(tStr+1);
    return(tStr);
}

char *getWeekDay(void)                 // giorno della settimana
{
    extern char *weekdays[];

    return(weekdays[tData->tm_wday]);
}

char *getWeekDay1(void)                // giorno della settimana 3 lettere
{
    extern char *weekdays[];

    weekdays[tData->tm_wday][WDAYSHORTLEN] = NULL;
    return(getWeekDay());
}

char *getWeekDayNum(void)              // giorno della settimana numero
{
    extern char tStr[];

    sprintf(tStr,"%1d",tData->tm_wday);
    return(tStr);
}

//-----------------------------------------------------------------------------
// definizione della struttura di gestione delle macro come tipo di dato e
// dichiarazione dell'array di strutture che definisce tutte le macro
//-----------------------------------------------------------------------------

typedef struct {             // struttura per la gestione delle macro
    char symbol;                // simbolo della macro
    char *(*replacer)(void);    // funz. che sostituisce il simbolo col signif.
    char *comment;              // commento; usato nello help
} MACRO;

MACRO macros[] = {
    {MACRO_FLAG,retMacroFlag,"il carattere macro"},
    {DQUOTE_CHR,retDoubleQuote,"le virgolette"},
    {'A',getYear,"l'anno; quattro cifre"},
    {'a',getYear1,"l'anno; due sole cifre"},
    {'M',getMonth,"il mese; sempre due cifre"},
    {'m',getMonth1,"il mese; una sola cifra se la prima è 0"},
    {'G',getDay,"il giorno del mese; sempre due cifre"},
    {'g',getDay1,"il giorno del mese; una sola cifra se la prima è 0"},
    {'R',getOrdinal,"il giorno dell'anno; sempre tre cifre"},
    {'r',getOrdinal1,"il giorno dell'anno; una o due cifre se le prime sono 0"},
    {'O',getHour,"l'ora; sempre due cifre"},
    {'o',getHour1,"l'ora; una sola cifra se la prima è 0"},
    {'I',getMin,"il minuto; sempre due cifre"},
    {'i',getMin1,"il minuto; una sola cifra se la prima è 0"},
    {'S',getSec,"il secondo; sempre due cifre"},
    {'s',getSec1,"il secondo; una sola cifra se la prima è 0"},
    {'E',getWeekDay,"il giorno della settimana; nome intero"},
    {'e',getWeekDay1,"il giorno della settimana; primi tre caratteri del nome"},
    {'W',getWeekDayNum,"il giorno della settimana; numero (0 = dom; 6 = sab)"},
    {NULL,NULL,NULL}
};

//-----------------------------------------------------------------------------
// organizzazione delle opzioni
//-----------------------------------------------------------------------------

#define  DISPLAY_ONLY        1         // opzione solo visualizza cmd line
#define  DAY_SHIFT           2         // opzione shift data in giorni

unsigned options;               // variabile per contenere i bits delle opzioni

char *optionS = "d:v";                 // stringa definizione opzioni

#pragma  warn -par
#pragma  warn -rvl

// l'opzione -d consente di specificare uno scostamento in giorni dalla data
// corrente. Lo scostamento puo' essere positivo (es: 1 = doamni) o negativo
// (es: -1 = ieri). La funzione esegue tutti i controlli formali e modifica
// di conseguenza i dati di lavoro.

int valid_d(struct OPT *vld,int cnt)                     // convalida opzione d
{
    register len, baseYear;
    long jul;
    extern struct tm *tData;
    extern unsigned options;

    len = strlen(vld->arg);
    if((len < 2) || (len > 4) || ((*vld->arg != '-') && (*vld->arg != '+')) ||
        (!isDigitStr(vld->arg+1)))
        fatalError(_E_BADOPTIONS);
    jul = date2jul(tData->tm_mday,tData->tm_mon,baseYear = tData->tm_year);
    jul += (len = atoi(vld->arg));
    jul2date(jul,&tData->tm_mday,&tData->tm_mon,&tData->tm_year);
    adjustOrdinal(baseYear,len);
    adjustWeekDay(len);
    return(options |= DAY_SHIFT);
}

// l'opzione -v forza DATECMD a non eseguire la command line costruita con
// l'espansione delle macro, bensi' a visualizzarla solamente. Qui viene
// settato il flag che indica che l'opzione e' stata richiesta

int valid_v(struct OPT *vld,int cnt)                     // convalida opzione v
{
    extern unsigned options;

    return(options |= DISPLAY_ONLY);
}

// gestione delle opzioni specificate in modo errato

int err_handle(struct OPT *vld,int cnt)                  // opzioni errate
{
    help();
    fatalError(_E_BADOPTIONS);
}

#pragma  warn .par
#pragma  warn .rvl

// array che associa ogni ozione alla corrispondente funzione di validazione

static struct VOPT vfuncs[] = {
    {'d',valid_d},
    {'v',valid_v},
    {ERRCHAR,err_handle},
    {NULL,NULL}
};


//-----------------------------------------------------------------------------
// corpo del programma (dopo main() le funzioni sono in ordine alfabetico)
//-----------------------------------------------------------------------------

int main(int argc,char **argv)                    // pilota tutte le operazioni
{
    char *initArgs;
    char *cmdLine;
    extern unsigned options;
    extern char *sys_errlist[];

    printf("%s %s - Esegue command lines con macro data/ora - Barninga_Z! '%s\n",_PRG_,_VERSION_,_YEAR_);
    initArgs = initProg(argc,argv);
    cmdLine = parseCmd(initArgs);
    printf("\n%s\n\n",cmdLine);
    if(!(options & DISPLAY_ONLY))
        if(system(cmdLine))
            return(printf("%s: %s\n",_PRG_,sys_errlist[errno]));
    return(0);
}

// adjustOrdinal() modifica il numero di giorno nell'anno (tm_yday) in base
// al numero di giorni di shift della data (opzione -d), tenendo presente che
// lo shift potrebbe generare un cambiamento di anno in piu' o in meno.

void adjustOrdinal(int baseYear,int shift)
{
    register diff, year;
    extern struct tm *tData;

    year = tData->tm_year;
    tData->tm_yday += shift;
    if(tData->tm_yday <= 0) {
        for(diff = 0; year < baseYear; year++)
            diff += YEARLEN+isleapyear(year);
        tData->tm_yday += diff;
    }
    else {
        for(diff = 0; baseYear < year; baseYear++)
            diff += YEARLEN+isleapyear(baseYear);
        tData->tm_yday -= diff;
    }
}

// adjustWeekDay() modifica il numero di giorno nella settimana (tm_wday) in
// base al numero di giorni di shift della data (opzione -d), tenendo presente
// che lo shift potrebbe generare un cambiamento di settimama.

void adjustWeekDay(int shift)
{
    register temp = 0;
    extern struct tm *tData;

    if((tData->tm_wday += shift) < 0) {
        tData->tm_wday = -tData->tm_wday;
        temp = 1;
    }
    tData->tm_wday %= WEEKLEN;
    if(temp && tData->tm_wday)
        tData->tm_wday = WEEKLEN-tData->tm_wday;
}

// fatalError() esce a DOS quando si verifica un errore, visualizzando un
// messaggio dall'array errors.

void fatalError(int errNum)
{
    printf("%s: %s",_PRG_,errors[errNum]);
    exit(1);
}

// help() stampa una videata di aiuto usando il campo comment di dell'array
// di strutture MACRO macros per visualizzare la descrizioni delle macro

void help(void)
{
    register i;
    extern MACRO macros[];
    extern char *helptext;

    printf(helptext);
    for(i = 0; macros[i].symbol; i++)
        printf("@%c => %s\n",macros[i].symbol,macros[i].comment);
}

// initProg() controlla i parametri e concatena gli elementi di argv per
// ricostruire la command line di DATECMD in un'unica stringa.

char *initProg(int argc,char **argv)
{
    register i;
    long timebits;
    struct OPT *optn;
    static char initArgs[MAXCMD];
    extern struct tm *tData;

    switch(argc) {
        case 1:
            help();
            fatalError(_E_BADARGS);
        default:
            time(&timebits);
            tData = localtime(&timebits);
            ++tData->tm_yday;                                        // 1 - 366
            ++tData->tm_mon;                                         // 1 - 12
            if(!(optn = parseopt(argc,argv,optionS,SWITCH,NULL,NULL,vfuncs)))
                fatalError(_E_ALLOCMEM);
    }
    *initArgs = NULL;
    for(i = argc-optn[0].val; ; ) {
        strcat(initArgs,argv[i]);
        if(++i < argc)
            strcat(initArgs," ");
        else
            break;
    }
    return(initArgs);
}

// isDigitStr() controlla se una stringa contiene solo 0-9

int isDigitStr(char *string)
{
    for(; *string; string++)
        if(!isdigit(*string))
            return(NULL);
    return(1);
}

// parseCmd() ricerca ed espande le macro presenti nella command line di
// DATECMD, costruendo il comando da eseguire, nel quale compaiono, in luogo
// delle macro, gli elementi di data e ora desiderati.

char *parseCmd(char *cmdArgs)
{
    register mIndex, i, j;
    char *ptr;
    static char cmdLine[MAXCMD];
    extern MACRO macros[];

    *cmdLine = NULL;
    for(i = 0, j = 0; cmdArgs[i]; i++) {
        switch(cmdArgs[i]) {
            case MACRO_FLAG:
                for(mIndex = 0; macros[mIndex].symbol; mIndex++) {
                    if(macros[mIndex].symbol == cmdArgs[i+1])
                        break;
                }
                if(macros[mIndex].symbol) {
                    ptr = (macros[mIndex].replacer)();
                    if((j += strlen(ptr)) < MAXCMD) {
                        strcat(cmdLine,ptr);
                        ++i;
                    }
                    else
                        fatalError(_E_CMDLONG);
                    break;
                }
            default:
                cmdLine[j++] = cmdArgs[i];
        }
    }
    return(cmdLine);
}

DATECMD può rivelarsi particolarmente utile quando vi sia la necessità di dare ad un file, mediante i comandi DOS COPY o REN, un nome dipendente dalla data e ora in cui l'operazione stessa è effettuata. Si pensi, ad esempio, ad una procedura che, quotidianamente, scrive il risultato delle proprie elaborazioni nel file OUTPUT.DAT: qualora sia necessario conservare a lungo i file generati, può risultare comodo riservare loro una directory e copiarveli di giorno in giorno, rinominandoli in modo appropriato. Il solo modo di inserire il comando in un file batch, senza necessità alcuna di intervento interattivo da parte dell'utente, consiste nel servirsi di DATECMD:

datecmd copy d:\proc\output.dat c:\proc\out\@a@M@G.dat

Se ogni file deve essere conservato per una settimana soltanto, il comando presentato necessita appena un piccolo aggiustamento:

datecmd copy d:\proc\output.dat c:\proc\out\output.@e

Infatti, dal momento che la macro @e viene sostituita dai tre caratteri iniziali del nome del giorno della settimana, è evidente che ogni giorno il nuovo file sovrascrive quello copiato sette giorni prima.

Va ancora sottolineato che DATECMD consente una gestione avanzata di piping e redirezione: ad esempio, il comando

datecmd copy d:\proc\output.dat c:\proc\out\@a@M@G.dat >> batch.ctl

redirige lo standard output di DATECMD in coda al file BATCH.CTL, mentre il comando

datecmd "copy d:\proc\output.dat c:\proc\out\@a@M@G.dat >> batch.ctl" > nul

aggiunge a BATCH.CTL lo standard output del comando COPY e sopprime quello di DATECMD; si noti l'uso delle virgolette, indispensabili per evitare che il DOS interpreti la redirezione facente parte della command line che deve essere eseguita da DATECMD [6].

File piccoli a piacere: FCREATE

Gli esempi presentati poco sopra nascondono una insidia: il comando DOS COPY fallisce se il file origine ha dimensione pari a 0 byte; la conseguenza è che nella directory dedicata alla memorizzazione dei file di output potrebbero mancarne alcuni[7].

FCREATE aggira il problema, consentendo la creazione di un file della dimensione voluta. Il file generato, qualora abbia dimensione maggiore di 0, contiene esclusivamente byte nulli (zero binario).

/******************************************************************************

    FCREATE.C - Barninga Z! - 27/10/1994

    Crea un file della dimensione indicata sulla riga di comando.

    Compilato sotto Borland C++ 3.1

    bcc fcreate.c

******************************************************************************/
#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys\stat.h>

#define  PRG       "FCREATE"
#define  VER       "1.0"
#define  YEAR      "94"

#define  OPENMODE    O_WRONLY+O_CREAT+O_TRUNC+O_BINARY
#define  OPENATTR    S_IREAD+S_IWRITE

#define  E_SYNTAX    1
#define  E_OPEN      2
#define  E_CHSIZE    3
#define  E_CLOSE     4
#define  E_PARM      5

// tutte le operazioni sono effettuate in main()

int main(int argc,char **argv)
{
    register handle, retcode;
    long size;

    fprintf(stderr,"%s %s - Creates file(s) of given size - Barninga Z! '%s\n",PRG,
        VER,YEAR);

// controllo dei parametri della command line

    if(argc != 3) {
        fprintf(stderr,"%s: Syntax is: %s size filename\n",PRG,PRG);
        return(E_SYNTAX);
    }

// controllo del valore di size

    if((size = atol(argv[1])) < 0L) {
        fprintf(stderr,"%s: requested size must be 0 or greater.\n",PRG);
        return(E_PARM);
    }
    retcode = 0;

// apertura del file: se non esiste viene creato; se esiste e' troncato a 0

    if((handle = open(argv[2],OPENMODE,OPENATTR)) == -1) {
        retcode = E_OPEN;
        printf("%s: error opening %s\n",PRG,argv[2]);
    }
    else {

// se il file e' stato aperto regolarmente se ne imposta la dimensione voluta
// con chsize()

        if(chsize(handle,size)) {
            retcode = E_CHSIZE;
            printf("%s: error sizing %s\n",PRG,argv[2]);
        }
        else

// se l'impostazione della nuova dimensione e' riuscita si calcola la nuova
// dimensione del file per verifica

            size = filelength(handle);

// chiusura del file

        if(close(handle)) {
            retcode = E_CLOSE;
            printf("%s: error closing %s\n",PRG,argv[2]);
        }
    }

// test e azione conseguente in caso di errore o meno

    if(!retcode)
        printf("%s: created %s (%ld bytes)\n",PRG,argv[2],size);
    return(retcode);
}

FCREATE richiede due parametri: il primo esprime la dimensione desiderata per il file; il secondo è il nome di questo. Insieme con EMPTYLVL e DATECMD è possibile implementare un algoritmo privo di criticità:

DIR "D:\PROC\OUTPUT.DAT" | FIND " 0 " | EMPTYLVL
IF ERRORLEVEL 1 GOTO NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE 0 BYTES: UTILIZZO FCREATE...
DATECMD FCREATE 0 C:\PROC\OUT\@a@M@G.OUT
GOTO END
:NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE MAGGIORE DI 0 BYTES: UTILIZZO COPY...
DATECMD COPY D:\PROC\OUTPUT.DAT C:\PROC\OUT\@a@M@G.OUT
:END

FCREATE può validamente essere utilizzato per generare file aventi la funzione di placeholder, cioè per riservare spazio ad utilizzi futuri.

Attendere il momento buono: TIMEGONE

Per rendere le cose più complicate, assumiamo che il drive D:, sul quale viene prodotto OUTPUT.DAT dalla ormai nota procedura, non sia un disco fisicamente presente nel personal computer su cui lavoriamo, ma si tratti, al contrario, di un drive remoto[8], reso disponibile da una seconda macchina: il server di rete. La procedura, a sua volta, è eseguita su un terzo personal computer, anch'esso collegato al network ed in grado di accedere al medesimo disco condiviso. E' evidente, a questo punto, che non ha alcun senso lanciare il nostro file batch prima che la procedura in questione abbia terminato le proprie elaborazioni. Nell'ipotesi che ciò avvenga poco prima delle 23:00, chi non desideri trascorrere le proprie serate alla tastiera, in trepidante attesa, deve necessariamente adottare qualche provvedimento.

TIMEGONE è... il provvedimento necessario[9]: esso controlla se è trascorsa l'ora (ed eventualmente la data) indicata e termina, valorizzando di conseguenza il registro ERRORLEVEL. In particolare, ERRORLEVEL vale 1 se l'ora è trascorsa; 0 se non lo è.

L'ora da tenere sotto controllo deve essere specificata sulla command line di TIMEGONE nel formato hhmmss (due cifre per l'ora, due per i minuti, due per i secondi): ad esempio, il comando

timegone 060400

indica le ore 6:04. I minuti e i secondi devono essere sempre specificati.

TIMEGONE consente di indicare, in aggiunta all'ora, anche una data: allo scopo sono riconosciute tre opzioni di command line: ­d consente di specificare il giorno, sempre espresso con due cifre. Ad esempio, il 12 del mese si indica con ­d12. L'opzione ­m specifica il mese: febbraio, per esempio, si indica con ­m02. Infine, l'opzione ­y permette di indicare l'anno, in 4 cifre. Il 1994, pertanto, si specifica con ­y1994. Il comando

timegone -d03 -m11 170000

richiede a TIMEGONE di verificare se siano trascorse le ore 17 del 3 novembre: dal momento che l'anno non è specificato, esso non viene controllato (il risultato del test è indipendente dall'anno di elaborazione).

Mentre l'indicazione di giorno, mese e anno è facoltativa, l'ora deve essere sempre specificata. Tuttavia, in luogo dell'espressione in formato hhmmss può essere digitato un asterisco ('*') per forzare il programma a ricavare date e ora dalla stringa assegnata alla variabile d'ambiente TIMEGONE: questa ha formato YYYYMMGGhhmmss (l'indicazione dell'ora è preceduta da quattro cifre per l'anno, due per il mese, due per il giorno); è lecito specificare zeri per anno, mese e giorno (sempre in numero di quattro, due e, rispettivamente, ancora due) se si desidera che siano ignorati. La condizione dell'esempio precedente può essere espressa valorizzando la variabile TIMEGONE con il comando

set timegone=00001103170000

ed eseguendo successivamente

timegone *

Le opzioni eventualmente specificate sulla riga di comando hanno precedenza rispetto alla variabile d'ambiente; questa, infine, è ignorata se a TIMEGONE è passata, quale parametro, l'ora. Così

timegone -d04 *

verifica se sono trascorse le 17 del 4 novembre (fermo restando il valore della variabile TIMEGONE dell'esempio precedente).

TIMEGONE riconosce una quarta opzione: ­r richiede che il risultato del test sia invertito (ERRORLEVEL vale 1 se data e ora non sono trascorse, 0 altrimenti).

Segue il listato, ampiamente commentato, del programma (PARSEOPT.OBJ e le funzioni di manipolazione delle date sono descritti a parte).

/******************************************************************************

    TIMEGONE.C - Barninga Z! - 29/09/94

    Setta errorlevel a seconda che la data e l'ora correnti superino o meno
    quelle richieste. Vedere helpStr per i dettagli della sintassi e delle
    opzioni.

    Compilato sotto Borland C++ 3.1

    bcc timegone.c isleapyear.obj parseopt.obj

******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dos.h>

#include "parseopt.h"

#define  YEAR       1
#define  MONTH      2
#define  DAY        4
#define  REVERSE    8
#define  SWCHAR     '-'
#define  ILLMSG     "Invalid option"
#define  YEARLEN    4
#define  MONTHLEN   2
#define  DAYLEN     2
#define  DATELEN    (YEARLEN+MONTHLEN+DAYLEN)
#define  TIMELEN    6
#define  DTLEN      (DATELEN+TIMELEN)
#define  TVARSYM    "*"
#define  TVARNAME   "TIMEGONE"

#define  PRG        "TIMEGONE"
#define  REL        "1.0"
#define  YR         "94"
#define  RETERR     255
#define  FALSE      0
#define  TRUE       1
#define  ASCIIBASE  0x30
#define  MIN_YEAR   1980
#define  MAX_YEAR   2099

char *helpStr = "\
\n\
options: one or more of the following:\n\
  -dDD    day DD of the month (always 2 digits).\n\
  -mMM    month MM of the year (always 2 digits).\n\
  -yYYYY  year YYYY (always 4 digits).\n\
  -r      reverse the result.\n\
\n\
time      time, in hhmmss format (always 6 digits). If time is the string \"*\",\n\
          the environment variable TIMEGONE is used. It must contain a\n\
          datetime string in YYYYMMGGhhmmss format; YYYY=0000, MM=00, GG=00\n\
          cause year, month and day respectively to be ignored.\n\
\n\
Command line parameters, if given, always override the environment variable.\n\
\n\
Errorlevel is set to 1 if the (date and) time specified has gone; it is set to\n\
0 if not yet. The -r option causes the result to be reversed (0 if gone, 1 if\n\
not yet). If an error occurs, Errorlevel is set to 255.\n\n\
";

int isleapyear(int year);

int main(int argc,char **argv);
int hasTimeGone(void);
int isDateValid(void);
int isNumeric(char *string);

int valid_d(struct OPT *vld,int cnt);
int valid_m(struct OPT *vld,int cnt);
int valid_r(struct OPT *vld,int cnt);
int valid_y(struct OPT *vld,int cnt);
int err_handle(struct OPT *vld,int cnt);

#pragma warn -rvl
#pragma warn -par
#pragma warn -aus

// l'opzione -d consente di specificare il giorno per comporre una data da
// controllare insieme all'ora. Qui sono effettuati tutti i controlli formali;
// 00 e' valido e indica che qualsiasi giorno va bene

int valid_d(struct OPT *vld,int cnt)
{
    extern unsigned options;
    extern struct date da;

    if(options & DAY)
        err_handle(NULL,NULL);
    if((strlen(vld->arg) != DAYLEN) || !isNumeric(vld->arg))
        err_handle(NULL,NULL);
    da.da_day = atoi(vld->arg);
    options |= DAY;
}

// l'opzione -m consente di specificare il mese per comporre una data da
// controllare insieme all'ora. Qui sono effettuati tutti i controlli formali;
// 00 e' valido e indica che qualsiasi mese va bene

int valid_m(struct OPT *vld,int cnt)
{
    extern unsigned options;
    extern struct date da;

    if(options & MONTH)
        err_handle(NULL,NULL);
    if((strlen(vld->arg) != MONTHLEN) || !isNumeric(vld->arg))
        err_handle(NULL,NULL);
    da.da_mon = atoi(vld->arg);
    options |= MONTH;
}

// l'opzione -r rovescia il risultato del test: se il momento e' trascorso
// ERRORLEVEL e' settato a 0 invece che a 1, e viceversa

int valid_r(struct OPT *vld,int cnt)
{
    extern unsigned options;

    options |= REVERSE;
}

// l'opzione -y consente di specificare il anno per comporre una data da
// controllare insieme all'ora. Qui sono effettuati tutti i controlli formali;
// 0000 e' valido e indica che qualsiasi anno va bene

int valid_y(struct OPT *vld,int cnt)
{
    extern unsigned options;
    extern struct date da;

    if(options & YEAR)
        err_handle(NULL,NULL);
    if((strlen(vld->arg) != YEARLEN) || !isNumeric(vld->arg))
        err_handle(NULL,NULL);
    da.da_year = atoi(vld->arg);
    options |= YEAR;
}

// controlla la validita' formale dell'ora indicata sulla command line. Il
// formato deve essere hhmmss. In luogo dell'ora sulla cmdline si puo'
// specificare un asterisco: in tal caso il programma verifica se esiste la
// variabile d'ambiente TIMEGONE e ricava data e ora dalla stringa as essa
// assegnata.

int valid_time(struct OPT *vld,int cnt)
{
    int dummy;
    static int instance;
    extern unsigned options;
    extern struct dostime_t ti;
    extern struct date da;

    if(instance++)
        err_handle(NULL,NULL);              // solo un'ora puo' essere indicata

// se e' specificato l'asterisco...

    if(!strcmp(vld->arg,TVARSYM)) {

// ...si cerca la variabile d'ambiente TIMEGONE: se e' presente se ne assegna
// l'indirizzo a vld->arg, in modo da poter simulare, dopo l'elaborazione
// della data, la digitazione di un'ora sulla command line

        if(!(vld->arg = getenv(TVARNAME))) {
            vld->arg = TVARNAME" environment variable missing";
            err_handle(vld,NULL);
        }

// si effettua il controllo di numericita' sulla stringa YYYYMMDD e si
// valorizzano i campi della struttura date

        if((strlen(vld->arg) != DTLEN) || !isNumeric(vld->arg)) {
            vld->arg = "illegal "TVARNAME" environment variable value";
            err_handle(vld,NULL);
        }
        if(sscanf(vld->arg,"%4d%02d%02d",
                             (options & YEAR) ? &dummy : (int *)&da.da_year,
                             (options & MONTH) ? &dummy : (int *)&da.da_mon,
                             (options & DAY) ? &dummy : (int *)&da.da_day) < 3)
            err_handle(NULL,NULL);

// vld->arg e' forzato a puntare alla seconda parte della stringa, avente
// formato hhmmss

        vld->arg += DATELEN;
    }
    else

// da qui in poi l'elaborazione e' identica sia nel caso di ora passata sulla
// command line che di utilizzo della variabile TIMEGONE, in quanto, in
// entrambi i casi, vld->arg punta a una stringa in formato hhmmss

        if((strlen(vld->arg) != TIMELEN) || !isNumeric(vld->arg))
            err_handle(NULL,NULL);

// valorizzazione dei campi della struttura dostime_t

    if(sscanf(vld->arg,"%02d%02d%02d",(int *)&ti.hour,
                                      (int *)&ti.minute,
                                      (int *)&ti.second) < 3)
        err_handle(NULL,NULL);

// controllo di validita' formale della data

    if(!isDateValid())
        err_handle(NULL,NULL);

// controllo di validita' formale dell'ora

    if((ti.hour > 23) || (ti.minute > 59) || (ti.second > 59))
        err_handle(NULL,NULL);
}

// gestione degli errori: visualizzazione della stringa di help e del valore
// di ERRORLEVEL

int err_handle(struct OPT *vld,int cnt)
{
    extern char *helpStr;

    if(vld)
        fprintf(stderr,"%s: %s.\n",PRG,vld->arg);
    fprintf(stderr,"%s: Usage: %s [option(s)] time | *\n%s",PRG,PRG,helpStr);
    printf("%s: errorlevel set to %d.\n",PRG,RETERR);
    exit(RETERR);                                             // non togliere!!
}

#pragma warn .par
#pragma warn .rvl
#pragma warn .aus

unsigned options;

// ogni opzione e' associata alla corrispondente funzione di validazione

struct VOPT valfuncs[] = {
    {'d',valid_d},
    {'m',valid_m},
    {'r',valid_r},
    {'y',valid_y},
    {ERRCHAR,err_handle},
    {NULL,valid_time},
};

// elenco delle opzioni: quelle seguite da ':' vogliono un argomento

char *optionS = "d:m:ry:";

struct date da;
struct dostime_t ti;

// main() analizza le opzioni via parseopt() ed effettua il test su data/ora
// via hasTimeGone()

int main(int argc,char **argv)
{
    register retval;

    printf("%s %s - Test if (date/)time has gone - Barninga Z! '%s.\n",PRG,REL,YR);
    if(argc < 2)
        err_handle(NULL,NULL);
    if(!parseopt(argc,argv,optionS,SWCHAR,ILLMSG,ILLMSG,valfuncs)) {
        perror(PRG);
        retval = RETERR;
    }
    else {
        retval = hasTimeGone();
        if(options & REVERSE)
            if(retval)
                retval = 0;
            else
                retval = 1;
    }
    printf("%s: errorlevel set to %d.\n",PRG,retval);
    return(retval);
}

// controlla se la data/ora attuale ha superato la data/ora specificate via
// opzioni e/o variabile d'ambiente

int hasTimeGone(void)
{
    char datetimeNow[DTLEN+1], datetime[DTLEN+1];
    struct date daNow;
    struct dostime_t tiNow;
    extern struct date da;
    extern struct dostime_t ti;

// richiesta a sistema di data e ora attuali

    getdate(&daNow);
    _dos_gettime(&tiNow);

// valorizzazione della stringa in formato YYYYMMDDhhmmss con data/ora attuali

    sprintf(datetimeNow,"%4d%02d%02d%02d%02d%02d",daNow.da_year,
                                                  daNow.da_mon,
                                                  daNow.da_day,
                                                  tiNow.hour,
                                                  tiNow.minute,
                                                  tiNow.second);

// valorizzazione stringa YYYYMMDDhhmmss con data/ora richieste, tenendo conto
// che un valore di 0 per anno, mese o giorno deve essere sostituito con oggi

    sprintf(datetime,"%4d%02d%02d%02d%02d%02d",da.da_year?da.da_year:daNow.da_year,
                                               da.da_mon ?da.da_mon :daNow.da_mon,
                                               da.da_day ?da.da_day :daNow.da_day,
                                               ti.hour,
                                               ti.minute,
                                               ti.second);

// confronto tra le due stringhe per determinare se il momento e' trascorso.
// Il confronto tra stringhe evita di confrontare uno a uno i campi numerici
// incrociando i risultati

    if(strcmp(datetimeNow,datetime) < 0)
        return(0);
    return(1);
}

// isDateValid() controlla la correttezza formale della data, verificando che
// per anno, mese e giorno siano verificati valori plusibili

int isDateValid(void)
{ 
    static int monLenTbl[] = {31,28,31,30,31,30,31,31,30,31,30,31};
    extern struct date da;

// se il gmese e' < 0 o > 12 errore

    if((da.da_mon < 0 ) || (da.da_mon > 12))
        return(0);

// se l'anno non e' 0 deve essere compreso tra gli estremi di validita'; se lo
// e' si determina se e' bisestile

    if(da.da_year) {
        if((da.da_year < MIN_YEAR) || (da.da_year > MAX_YEAR))
            return(0);
        else
            monLenTbl[1] += isleapyear(da.da_year);
    }
    else

// se l'anno e' 0 occorre ammettere che potrebbe essere bisestile, chissa'...

        monLenTbl[1] += 1;

// se il giorno non e' 0 esso deve essere positivo e minore o uguale al numero
// di giorni del mese; se il mese e' 0 bisogna ammettere che il 31 e' valido

    if(da.da_day) {
        if(da.da_day < 0)
            return(0);
        if(da.da_mon) {
            if(da.da_day > monLenTbl[da.da_mon-1])
                return(0);
        }
        else
            if(da.da_day > 31)                   // sempre e comunque errore!!
                return(0);
    }
    return(1);                                   // 1 = data OK
}

// controlla la numericita' della stringa ricevuta come parametro

int isNumeric(char *string)
{
    register i;

    for(; *string; string++) {
        for(i = (ASCIIBASE+0); i < (ASCIIBASE+10); i++)
            if(*string == i)
                break;
        if(i == (ASCIIBASE+10))
            return(FALSE);
    }
    return(TRUE);
}

Vediamo, finalmente, TIMEGONE al lavoro nel nostro esempio pratico:

:ATTESA
TIMEGONE 230000
IF ERRORLEVEL 1 GOTO ADESSO
GOTO ATTESA
:ADESSO
ECHO LE ORE 23:00 SONO APPENA TRASCORSE
DIR "D:\PROC\OUTPUT.DAT" | FIND " 0 " | EMPTYLVL
IF ERRORLEVEL 1 GOTO NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE 0 BYTES: UTILIZZO FCREATE...
DATECMD FCREATE 0 C:\PROC\OUT\@a@M@G.OUT
GOTO END
:NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE MAGGIORE DI 0 BYTES: UTILIZZO COPY...
DATECMD COPY D:\PROC\OUTPUT.DAT C:\PROC\OUT\@a@M@G.OUT
:END

Il batch può essere eseguito a qualsiasi ora: TIMEGONE forza un loop sulle prime quattro righe del file sino alle 23:00 (se il batch è lanciato dopo le 23 il flusso elaborativo salta immediatamente all'etichetta :ADESSO).

Estrarre una sezione da un file: SELSTR

Sempre più difficile: il famigerato OUTPUT.DAT contiene una sezione di testo che costituisce la base per alcune postelaborazioni. Ipotizziamo che essa abbia il seguente layout:

* RIEPILOGO *
....
* FINE *

Il contenuto della sezione, per il momento, non è rilevante: l'obiettivo è estrarla in modo automatico da OUTPUT.DAT e scriverla in un secondo file, da processare in seguito. La utility SELSTR rappresenta una soluzione: il comando

selstr "* RIEPILOGO *" "* FINE *" < output.dat > riepilog.dat

scrive sullo standard output le righe di testo, lette dallo standard input, a partire da quella contenente la prima stringa specificata e conclude l'estrazione con la riga contenente la seconda. Ancora una volta, perciò, è possibile sfruttare le capacità di redirezione offerte dal sistema operativo.

Tuttavia, SELSTR è in grado di fare di più. Esso, infatti, accetta sulla riga di comando alcune opzioni tramite le quali è possibile modificarne il comportamento, indicando se l'estrazione debba iniziare o terminare ad una riga specificata: per la descrizione dettagliata delle opzioni e dei loro parametri si veda il testo assegnato alla variabile helpStr, nel listato del programma. Qui vale la pena di soffermarsi sull'opzione ­c, che consente di determinare la modalità di caching [10] degli stream. In particolare, ­ci richiede a SELSTR di attivare il caching dello standard input­co richiede l'attivazione del caching per lo standard output; infine, ­cb attiva il caching per entrambi gli stream. L'uso attento dell'opzione ­c permette di ottenere un significativo incremento della efficienza di elaborazione.

Segue il listato, ampiamente commentato, del programma (vedere il paragrafo dedicato circa l'implementazione di PARSEOPT.OBJ).

/******************************************************************************

    SELSTR.C - Barninga Z! - 20/09/94

    Seleziona una porzione di file in base a stringhe specificate come
    delimitatori di inizio o fine. Vedere helpStr per i dettagli.

    Compilato sotto Borland C++ 3.1

    bcc selstr.c parseopt.obj

******************************************************************************/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>

#include "parseopt.h"

#define  PRG        "SELSTR"
#define  VER        "1.0"
#define  YEAR       "94"
#define  MAXLIN     1024
#define  RET_ERR    255
#define  BUFSIZE    16348

#define  FROMNUM    1
#define  FROMBEG    2
#define  FROMLIN    4
#define  TONUM      8
#define  CACHE_I    16
#define  CACHE_O    32
#define  FROMBEG_C  'b'
#define  FROMLIN_C  'l'
#define  CACHE_I_C  'i'
#define  CACHE_O_C  'o'
#define  CACHE_B_C  'b'
#define  SWCHAR     '-'
#define  ILLMSG     "Invalid option"

int main(int argc,char **argv);
int search(char *string1,char *string2);

int valid_c(struct OPT *vld,int cnt);
int valid_f(struct OPT *vld,int cnt);
int valid_t(struct OPT *vld,int cnt);
int err_handle(struct OPT *vld,int cnt);
int ctl_strings(struct OPT *vld,int cnt);

unsigned long startNum = 1L;
unsigned long stopNum = 0xFFFFFFFFL;

char *helpStr = "\
options:\n\
-c: cache data streams:\n\
     -ci: cache standard input\n\
     -co: cache standard output\n\
     -cb: cache both stdin and stdout\n\
-f: line selection origin mode:\n\
     -fn: from linenumber: lines are selected from line n (n > 0) to the\n\
          line containing string1 or to the line defined by -t, whichever\n\
          comes first.\n\
     -fb: from beginning: lines are selected from BOF to the line\n\
          containing string1 or to the line defined by -t, whichever comes\n\
          first. Same as -f1.\n\
     -fl: from line (default mode): lines are selected from the line\n\
          containing string1 to EOF. Selection stops at the line\n\
          containing string2, if given and found, or at the line defined by -t.\n\
-t: line selection end mode:\n\
     -tn: to linenumber: lines are selected from the origin defined by -f\n\
          to line n (n > 0). Anyway, selection stops at the line\n\
          containing string1 (or string2 for -fl), if found.\n\
ErrorLevel: 255: error;\n\
              1: ok, one or more lines selected;\n\
              0: ok, no lines selected.\
";

unsigned options = FROMLIN;

#pragma warn -rvl
#pragma warn -par
#pragma warn -aus

// validazione dell'opzione -c per la richiesta di caching degli streams.
// Sono attivati i buffers di cache richiesti

int valid_c(struct OPT *vld,int cnt)
{
    static int instance;
    extern unsigned options;

    if(instance++ || (strlen(vld->arg) != 1))
        err_handle(NULL,NULL);
    switch(*vld->arg) {
        case CACHE_B_C:
        case CACHE_I_C:
            if(setvbuf(stdin,NULL,_IOFBF,BUFSIZE)) {
                perror(PRG);
                exit(RET_ERR);
            }
            options |= CACHE_I;
            if(*vld->arg == CACHE_I_C)
                break;
        case CACHE_O_C:
            if(setvbuf(stdout,NULL,_IOFBF,BUFSIZE)) {
                perror(PRG);
                exit(RET_ERR);
            }
            options |= CACHE_O;
            break;
        default:
            err_handle(NULL,NULL);
    }
}

// l'opzione -f consente di specificare quale deve essere la prima linea
// copiata da stdin a stdout: quella contenente il primo nonoption item
// (default), quella il cui numero e' specificato come argomento, o la prima
// riga del file

int valid_f(struct OPT *vld,int cnt)
{
    char *ptr;
    extern unsigned options;
    static int instance;

    if(instance++)
        err_handle(NULL,NULL);
    for(ptr = vld->arg; *ptr; ptr++)
        if(!isdigit(*ptr))
            if(strlen(vld->arg) > 1)
                err_handle(NULL,NULL);
            else {
                switch(*vld->arg) {
                    case FROMLIN_C:
                        break;                   // FROMLIN settato per default
                    case FROMBEG_C:
                        options &= ~FROMLIN;
                        options |= FROMBEG;
                        break;                   // startNum = 1 per default
                    default:
                        err_handle(NULL,NULL);
                }
                return;
            }
    startNum = atol(vld->arg);
    options &= ~FROMLIN;
    options |= FROMNUM;
}

// l'opzione -t consente di specificare quale deve essere l'ultima riga del
// file copiata da stdin a stdout. -t consente di indicare un numero di riga;
// se -t non e' specificata la selezione termina alla riga contenente il
// secondo nonoption item sulla command line; in assenza di questo la selezione
// termina a fine file

int valid_t(struct OPT *vld,int cnt)
{
    char *ptr;
    static int instance;
    extern unsigned options;
    extern unsigned long stopNum;

    if(instance++)
        err_handle(NULL,NULL);
    for(ptr = vld->arg; *ptr; ptr++)
        if(!isdigit(*ptr))
            err_handle(NULL,NULL);
    stopNum = atol(vld->arg);
    options |= TONUM;
}

// gsetione errore di sintassi: visualizzazione del messaggio di help e
// uscita con errore

int err_handle(struct OPT *vld,int cnt)
{
    extern char *helpStr;

    fprintf(stderr,"%s: Usage: %s [option(s)] string1 [string2]\n%s",PRG,PRG,
                                                                      helpStr);
    exit(RET_ERR);                                            // non togliere!!
}

// controllo dei parametri non-opzione presenti sulla command line: possono
// essere nessuno, uno o due a seconda delle opzioni richieste

int ctl_strings(struct OPT *vld,int cnt)
{
    static int instance;

    switch(instance) {
        case 0:
            break;                                    // string1 sempre ammessa
        case 1:
            if(!(options & FROMLIN))
                err_handle(NULL,NULL);      // string2 ammessa solo se c'e' -fl
            break;
        default:
            err_handle(NULL,NULL);                          // troppi parametri
    }
    return(++instance);
}

#pragma warn .par
#pragma warn .rvl
#pragma warn .aus

// array di strutture che associano ad ogni opzione la corrispondente
// funzione di validazione

struct VOPT valfuncs[] = {
    {'c',valid_c},
    {'f',valid_f},
    {'t',valid_t},
    {ERRCHAR,err_handle},
    {NULL,ctl_strings},
};

// stringa elencante le opzioni ammesse. Quelle seguite da ':' richiedono un
// argomento

char *optionS = "c:f:t:";

// main() scandisce la command line via parseopt() e, se le opzioni sono
// regolari, procede nell'elaborazione via search()

int main(int argc,char **argv)
{
    struct OPT *opt;

    fprintf(stderr,"%s %s - line selection from stdin to stdout - Barninga Z! '%s\n",
                                                                 PRG,VER,YEAR);
    if(argc == 1)
        err_handle(NULL,NULL);                                  // non ritorna!
    if(!(opt = parseopt(argc,argv,optionS,SWCHAR,ILLMSG,ILLMSG,valfuncs))) {
        perror(PRG);
        return(RET_ERR);
    }
    if(!opt[0].val)
        err_handle(NULL,NULL);                              // non c'e' string1
    return(search(argv[argc-opt[0].val],argv[argc-opt[0].val+1]));
}

// search() legge stdin riga per riga e, secondo le opzioni e le stringhe
// specificate sulla riga di comando seleziona le righe da visualizzare su
// stdout

int search(char *string1,char *string2)
{
    register lFlag = 0;
    char line[MAXLIN];
    unsigned long i;
    extern unsigned options;
    extern unsigned long startNum, stopNum;

// dapprima sono scartate tutte le righe che precedono la prima da selezionare
// (se non e' stato specificata -fnum allora startNum e' 0 e non e' scartata
// alcuna riga)

    for(i = 1L; i < startNum; i++)
        fgets(line,MAXLIN,stdin);

// se la selezione deve iniziare alla riga contenente string1, questa e'
// cercata nel file e si comincia a copiare righe su stdout non appena essa
// e' trovata

    if(options & FROMLIN) {
        for(; fgets(line,MAXLIN,stdin); i++)
            if(strstr(line,string1)) {
                lFlag = printf(line);
                break;
            }

// la selezione di righe prosegue fino a che non e' incontrata la riga finale
// definita da stopNum (-t) o segnalata dalla presenza di string2

        for(; fgets(line,MAXLIN,stdin) && (i < stopNum); i++) {
            printf(line);
            if(string2)
                if(strstr(line,string2))
                    break;
        }
    }

// se la selezione deve partire dalla prima riga allora si copiano su stdout
// tutte le righe sino a quella contenente string1, che in questo caso
// permette di selezionare la riga di stop. Se era stato specificata una riga
// di stop con -t, allora e' controllata stopNum (che altrimenti contiene il
// massimo valore esprimible con un long, rendendo ininfluente il test)

    else {
        for(; fgets(line,MAXLIN,stdin) && (i < stopNum); i++) {
            lFlag = printf(line);
            if(strstr(line,string1))
                break;
        }
    }
    return(lFlag ? 1 : 0);       // 1 = copiate righe; 0 = nessuna riga copiata
}

Ecco la nuova versione del nostro file batch:

:ATTESA
TIMEGONE 230000
IF ERRORLEVEL 1 GOTO ADESSO
GOTO ATTESA
:ADESSO
ECHO LE ORE 23:00 SONO APPENA TRASCORSE
DIR "D:\PROC\OUTPUT.DAT" | FIND " 0 " | EMPTYLVL
IF ERRORLEVEL 1 GOTO NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE 0 BYTES: UTILIZZO FCREATE...
DATECMD FCREATE 0 C:\PROC\OUT\@a@M@G.OUT
GOTO END
:NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE MAGGIORE DI 0 BYTES: UTILIZZO COPY...
DATECMD COPY D:\PROC\OUTPUT.DAT C:\PROC\OUT\@a@M@G.OUT
ECHO GENERAZIONE DEL FILE DI RIEPILOGO...
SELSTR -cb "* RIEPILOGO *" "* FINE *" < D:\PROC\OUTPUT.DAT > C:\PROC\RIEPILOG.DAT
IF ERRORLEVEL 255 GOTO SELERROR
IF ERRORLEVEL 1 GOTO END
ECHO IL FILE OUTPUT.DAT NON CONTIENE LA SEZIONE DI RIEPILOGO
GOTO END
:SELERROR
ECHO ERRORE NELLA GENERAZIONE DEL RIEPILOGO
:END

La sezione estratta da OUTPUT.DAT, presente sul disco remoto, è scritta nel file RIEPILOG.DAT, sul disco locale. Dal momento che SELSTR restituisce in ERRORLEVEL un codice indicativo dello stato di uscita, è possibile effettuare in modo semplice la gestione degli errori: come si vede, in caso di errore di elaborazione viene restituito 255: il flusso del file batch salta all'etichetta SELERROR ed è visualizzato un opportuno messaggio. Se ERRORLEVEL vale 1, significa che l'elaborazione è terminata regolarmente ed è stata estratta almeno una riga di testo; altrimenti ERRORLEVEL contiene necessariamente 0, che indica terminazione regolare, ma senza estrazione di testo: non rimane che segnalare che il contenuto di OUTPUT.DAT non è quello atteso.

Estrarre colonne di testo da un file: CUT

A questo punto vi è la necessità (ebbene sì) di postelaborare RIEPILOG.DAT: a scopo di esempio, ipotizziamo che tra riga iniziale e finale ne sia presente un numero variabile, tutte aventi il formato descritto di seguito:

+-CODICE----DATA----FILE----IMPORTO-+

La sezione estratta, priva di riga inizale e finale, ha pertanto un aspetto analogo a quello delle righe seguenti:

099355476719940722OPER000100120344720
098446273319940722OPER003400009873325
088836288219940831OPER102800014436255
....
094553728219940912OPER001700225467520
062253725519941004OPER013100067855490
084463282919941103OPER000700127377385

E' necessario generare una tabella, da scrivere in un nuovo file, in cui solamente i campi DATA, IMPORTO e CODICE siano riportati in modo leggibile e nell'ordine indicato. E' inoltre necessario generare un secondo file, ogni riga del quale contenga esclusivamente il campo FILE, a scopo di elaborazione successiva.

Strumento atto allo scopo è la utility CUT, ispirata all'omonimo comando Unix, del quale implementa le caratteristiche principali (in sostanza, la capacità di estrarre colonne di testo da un file), affiancandovi alcune novità.

CUT legge lo standard input, da ogni riga del quale estrae i caratteri occupanti le posizioni specificate mediante l'opzione della command line ­c e li scrive sullo standard output. Tutti i messaggi sono scritti sullo standard error, al fine di non influenzare l'eventuale redirezione dell'output prodotto.

E' possibile specificare più istanze dell'opzione ­c per richiedere l'estrazione di più colonne di testo: queste possono sovrapporsi parzialmente o totalmente; inoltre la posizione di partenza può essere superiore a quella di arrivo (in questo caso CUT estrae i caratteri da destra a sinistra). Vediamo un esempio: nell'ipotesi che lo standard input di CUT sia costituito dalla riga

ABCDE12345FGHIJ67890

il comando

cut -c1-3 -c2-5 -c8-5 -c15-25

scrive sullo standard output la riga

ABCBCDE321EJ67890

Si noti che le posizioni dei caratteri sono numerate a partire da 1. Inoltre, per default, laddove la lunghezza della riga di standard input non sia sufficiente a soddisfare interamente uno degli intervalli specificati (come nel caso ­c15-25), vengono comunque estratti almeno i caratteri presenti: sono disponibili, però, tre opzioni di command line con le quali è possibile gestire tali situazioni in modo differente. In particolare, ­d forza CUT a scartare la riga di input, mentre ­x determina l'interruzione dell'elaborazione con ERRORLEVEL pari a 255 (il valore di default per una terminazione regolare è 0). Ancora, l'opzione ­p consente di specificare una stringa dalla quale estrarre i caratteri necessari a compensare quelli mancanti nello standard input: il comando

cut -p#=@ -c15-25

estrae dallo standard input dell'esempio precedente la riga

J67890#=@#=

Alcuni caratteri della stringa "#=@" sono ripetuti, in quanto la sua lunghezza non è sufficiente; al contrario, i caratteri eventualmente eccedenti la lunghezza necessaria sono ignorati.

CUT riconosce inoltre alcune opzioni che consentono di inserire stringhe in testa (­b) e in coda (­e) alle righe di standard output, nonché tra gli intervalli di caratteri (­m). Vediamone un esempio di utilizzo nel solito file batch:

:ATTESA
TIMEGONE 230000
IF ERRORLEVEL 1 GOTO ADESSO
GOTO ATTESA
:ADESSO
ECHO LE ORE 23:00 SONO APPENA TRASCORSE
DIR "D:\PROC\OUTPUT.DAT" | FIND " 0 " | EMPTYLVL
IF ERRORLEVEL 1 GOTO NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE 0 BYTES: UTILIZZO FCREATE...
DATECMD FCREATE 0 C:\PROC\OUT\@a@M@G.OUT
GOTO END
:NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE MAGGIORE DI 0 BYTES: UTILIZZO COPY...
DATECMD COPY D:\PROC\OUTPUT.DAT C:\PROC\OUT\@a@M@G.OUT
ECHO GENERAZIONE DEL FILE DI RIEPILOGO...
SELSTR -cb "* RIEPILOGO *" "* FINE *" < D:\PROC\OUTPUT.DAT > C:\PROC\RIEPILOG.DAT
IF ERRORLEVEL 255 GOTO SELERROR
IF ERRORLEVEL 1 GOTO POSTELAB
ECHO IL FILE OUTPUT.DAT NON CONTIENE LA SEZIONE DI RIEPILOGO
GOTO END
:SELERROR
ECHO ERRORE NELLA GENERAZIONE DEL RIEPILOGO
:POSTELAB
ECHO GENERAZIONE DELLA TABELLA DI RIEPILOGO...
TYPE C:\PROC\RIEPILOG.DAT FIND /V "* RIEPIL" | FIND /V "* FINE *" > C:\PROC\TR.TMP
ECHO TABELLA DI RIEPILOGO > C:\PROC\TR.TXT
ECHO. >> C:\PROC\TR.TXT
ECHO    DATA       IMPORTO       CODICE >> C:\PROC\TR.TXT
ECHO +-------------------------------------+ >> C:\PROC\TR.TXT
CUT -b"³ " -m" ³ " -e" ³" -c11-18 -c27-37 -c1-10 < C:\PROC\TR.TMP >> C:\PROC\TR.TXT
ECHO +-------------------------------------+ >> C:\PROC\TR.TMP
ECHO GENERAZIONE DELLA LISTA DI FILES...
CUT -c19-26 < C:\PROC\TR.TMP > C:\PROC\FILES.DAT
DEL C:\PROC\TR.TMP
:END

Il comando FIND è utilizzato con l'opzione /V per generare un file temporaneo (TR.TMP) contenente tutte le righe di RIEPILOG.DAT, eccetto la prima e l'ultima (intestazione e chiusura); TR.TMP è poi elaborato con CUT per produrre la tabella desiderata (TR.TXT) e il file contenente le sole occorenze del campo FILES.

Il contenuto del file TR.TXT è il seguente:

TABELLA DI RIEPILOGO

    DATA       IMPORTO       CODICE

 19940722
 00120344720
 0993554767
 19940722
 00009873325
 0984462733
 19940831
 00014436255
 0888362882
....
 19940912
 00225467520
 0945537282
 19941004
 00067855490
 0622537255
 19941103
 00127377385
 0844632829

Il contenuto di FILES.DAT è invece:

OPER0001
OPER0034
OPER1028
....
OPER0017
OPER0131
OPER0007

Segue il listato commentato di CUT (vedere il paragrafo dedicato circa l'implementazione di PARSEOPT.OBJ).

/******************************************************************************

    CUT.C - Barninga Z! - 1994

    Utility che riprende ed amplia le funzionalita' del comando CUT di Unix.
    Circa la sintassi vedere la variabile helpStr.

    Compilato sotto Borland C++ 3.1

    bcc -O -d -rd -k- cut.c parseopt.obj

******************************************************************************/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>

#include "parseopt.h"

#define  PRG         "CUT"         // nome del programma
#define  REL         "1.0"         // versione
#define  YEAR        "94"          // anno di rilascio
#define  ERRCOD      255           // codice errore uscita programma
#define  MAXLIN      2048          // massima lunghezza di una riga
#define  MAXPAD      128           // max lungh. stringa di padding
#define  CUTLEN      4             // max 4 cifre per opz. -c
#define  CUTOPT      1             // -c
#define  EXITOPT     2             // -x
#define  DISCRDOPT   4             // -d
#define  PADOPT      8             // -p
#define  MIDOPT      16            // -s
#define  BEGOPT      32            // -b
#define  ENDOPT      64            // -e
#define  SWITCH      '-'           // switch character per opzioni

// CUTTER e' il template di struttura che controlla la formazione delle
// colonne di testo: start contiene l'offset del primo byte; stop quello
// dell'ultimo. In pratica sono gli estremi del range da copiare. start
// puo' essere maggiore di stop, nel qual caso i bytes sono copiati in
// ordine inverso

typedef struct {
    unsigned start;
    unsigned stop;
} CUTTER;

char *helpStr = "\
Syntax is: cut option(s)\n\
Default exit code: 0. Input: stdin; Output: stdout; Msgs: stderr. Options are:\n\
-bBEGSTR      BEGSTR is the string to be inserted at the beginning of each\n\
              output line.\n\
-cSTART-STOP  START-STOP specify a range of characters to be copied from each\n\
              input line to output. If START is greater than STOP, characters\n\
              in the range are reversed. More than one range can be given;\n\
              anyway, at least one range is required. See also -d -p -x.\n\
-d            If one or more -c ranges fall outside an input line, the line\n\
              itself will be discarded. By default, columns are output even\n\
              if one or more ranges are not full. -d -p -x are mutually\n\
              exclusive.\n\
-eENDSTR      ENDSTR is the string to be appended at the end of each output\n\
              line.\n\
-mMIDSTR      MIDSTR is the string to be inserted between two -c ranges; useful\n\
              only if more than one range is given.\n\
-pPADSTR      PADSTR is the string used to pad output columns of -c ranges\n\
              falling outside a stdin line. When -p is not requested, output\n\
              lines can have different length. -p -d -x are mutually exclusive.\n\
-x            If one or more -c ranges fall outside an input line, the program\n\
              will immediately return 255. -x -d -p are mutually exclusive.\n\
-?            Displays this help screen and exits with a return code of 255.\
";

char *crgtMsg = "%s %s: cuts stdin into columns. Barninga Z! '%s. Help: -?\n";
char *eLinEnd = "Cut pointers past end of line";
char *eOptCut = "No cut option specified";
char *eOptFmt = "Invalid option format";
char *eOptGen = "Invalid option or option format";
char *eOptSeq = "Invalid option sequence";

CUTTER *cutArr;               // struttura di controllo
char padStr[MAXPAD];          // stringa per il padding
char begStr[MAXPAD];          // stringa di apertura
char midStr[MAXPAD];          // stringa di fincatura
char endStr[MAXPAD];          // stringa di chiusura

int valid_b(struct OPT *vld,int cnt);
int valid_c(struct OPT *vld,int cnt);
int valid_d(struct OPT *vld,int cnt);
int valid_e(struct OPT *vld,int cnt);
int valid_m(struct OPT *vld,int cnt);
int valid_p(struct OPT *vld,int cnt);
int valid_x(struct OPT *vld,int cnt);
int hlp_handle(struct OPT *vld,int cnt);
int err_handle(struct OPT *vld,int cnt);

int main(int argc,char **argv);
void cutstream(char *line,char *buffer,char *bufprint,int maxlen);

#pragma  warn -par
#pragma  warn -rvl

// se specificata opzione -b la stringa argomento deve essere copiata nello
// spazio apposito (begStr). Questa stringa e' scritta in testa ad ogni riga
// di standard output prodotta dal programma.

int valid_b(struct OPT *vld,int cnt)
{
    static int instance;
    extern char *eOptSeq;
    extern unsigned options;
    extern char begStr[];

    if(instance++)
        err_handle(NULL,(int)eOptSeq);
    strcpy(begStr,vld->arg);
    return(options |= BEGOPT);
}

// almeno una opzione -c deve sempre essere specificata. Qui vengono effettuati
// tutti i controlli di liceita' sull'argomento (estremi del range di colonne
// da estrarre da stdin) e per ogni range e' allocata una struttura CUTTER,
// formando cosi' un array di strutture che descrivono come deve essere
// "ritagliato" stdin.

int valid_c(struct OPT *vld,int cnt)
{
    register i, flag;
    static int instance;
    extern char *eOptFmt;
    extern unsigned options;
    extern CUTTER *cutArr;

    if(!isdigit(*(vld->arg)))
        err_handle(NULL,(int)eOptFmt);    // errore: primo car. non e' numerico
    for(i = 1, flag = 0; vld->arg[i]; i++)
        if(vld->arg[i] == '-') {             // un - puo' (deve) esserci
            if(flag)
                err_handle(NULL,(int)eOptFmt);  // errore: gia' trovato un -
            if(i > CUTLEN)
                err_handle(NULL,(int)eOptFmt);  // errore: numero troppo alto
            flag = i+1;                         // indice inizio secondo numero
        }
        else
            if(!isdigit(vld->arg[i]))
                err_handle(NULL,(int)eOptFmt); // errore: non e' - o una cifra
    if(!flag)
        err_handle(NULL,(int)eOptFmt);          // errore: non c'e' il -
    if((i-flag) > CUTLEN)
        err_handle(NULL,(int)eOptFmt);          // errore: numero troppo alto
    if(!cutArr)                                           // solo prima volta!!
        if(!(cutArr = (CUTTER *)malloc(sizeof(CUTTER))))
            err_handle(NULL,(int)sys_errlist[errno]);
    sscanf(vld->arg,"%d-%d",&(cutArr[instance].start),&(cutArr[instance].stop));
    if(!cutArr[instance].start || !cutArr[instance].stop)
        err_handle(NULL,(int)eOptFmt);
    ++instance;
    if(!(cutArr = (CUTTER *)realloc(cutArr,(instance+1)*sizeof(CUTTER))))
        err_handle(NULL,(int)sys_errlist[errno]);
    cutArr[instance].start = cutArr[instance].stop = NULL;
    return(options |= 1);
}

// l'opzione -d richiede che se uno o entrambi gli estermi di un range (-c)
// "cadono" al di fuori della riga attualmente processata la riga stessa
// deve essere scartata. L'opzione -d e' alternativa a -p e -x.

int valid_d(struct OPT *vld,int cnt)
{
    static int instance;
    extern char *eOptSeq;
    extern unsigned options;

    if(instance++ || (options & PADOPT+EXITOPT))
        err_handle(NULL,(int)eOptSeq);
    return(options |= DISCRDOPT);
}

// se specificata opzione -e la stringa argomento deve essere copiata nello
// spazio apposito (endStr). Questa stringa e' scritta in coda ad ogni riga
// di standard output prodotta dal programma.

int valid_e(struct OPT *vld,int cnt)
{
    static int instance;
    extern char *eOptSeq;
    extern unsigned options;
    extern char endStr[];

    if(instance++)
        err_handle(NULL,(int)eOptSeq);
    strcpy(endStr,vld->arg);
    return(options |= ENDOPT);
}

// se specificata opzione -m la stringa argomento deve essere copiata nello
// spazio apposito (midStr). Questa stringa e' scritta, in ogni riga di
// standard output prodotta dal programma, tra due range di caratteri estratti
// (-c) da stdin, se almeno 2 istanze di -c sono state specificate. Se ne e'
// stata richiesta una sola, l'opzione -m e' ignorata.

int valid_m(struct OPT *vld,int cnt)
{
    static int instance;
    extern char *eOptSeq;
    extern unsigned options;
    extern char midStr[];

    if(instance++)
        err_handle(NULL,(int)eOptSeq);
    strcpy(midStr,vld->arg);
    return(options |= MIDOPT);
}

// se specificata opzione -p la stringa argomento deve essere copiata nello
// spazio apposito (padStr). Questa stringa e' utilizzata, in ogni riga di
// standard output prodotta dal programma, per riempire il range richiesto
// (-c), se il numero di caratteri estratti da stdin e' minore dell'ampiezza
// specificata per il range stesso. E' significativa solo se uno o entrambi
// gli estremi di un range "cadono" al di fuori della riga di stdin attualmente
// processata. E' alternativa a -d e -x.

int valid_p(struct OPT *vld,int cnt)
{
    static int instance;
    extern char *eOptSeq;
    extern unsigned options;
    extern char padStr[];

    if(instance++ || (options & DISCRDOPT+EXITOPT))
        err_handle(NULL,(int)eOptSeq);
    strcpy(padStr,vld->arg);
    return(options |= PADOPT);
}

// l'opzione -x richiede che se uno o entrambi gli estermi di un range (-c)
// "cadono" al di fuori della riga attualmente processata l'elaborazione deve
// essere interrotta e restituito un codice di errore. Alternativa a -p e -x.

int valid_x(struct OPT *vld,int cnt)
{
    static int instance;
    extern char *eOptSeq;
    extern unsigned options;

    if(instance++ || (options & DISCRDOPT+PADOPT))
        err_handle(NULL,(int)eOptSeq);
    return(options |= EXITOPT);
}

// visualizza la videata di help se e' specificata opzione -?

int hlp_handle(struct OPT *vld,int cnt)
{
    extern char *helpStr;

    err_handle(NULL,(int)helpStr);
}

// gestisce tutte le situazioni in cui e' necessario visualizzare un messaggio
// ed uscire con un errore. Se il parametro vld e' NULL significa che la
// chiamata e' fatta esplicitamente dal programmatore e quindi e' visualizzato
// il messaggio il cui indirizzo e' passato come secondo parametro (con
// opportuno cast!). Se vld non e' NULL, allora err_handle() e' chiamata da
// parseopt() e quindi e' visualizzato un messaggio generico.

int err_handle(struct OPT *vld,int cnt)
{
    extern char *eOptGen;

    fprintf(stderr,"%s: %s\n",PRG,vld ? eOptGen : (char *)cnt);
    exit(ERRCOD);
}

#pragma  warn .par
#pragma  warn .rvl

// array che associa le opzioni alla funzione di validazione corrispondente

struct VOPT vfuncs[] = {
    {'b',valid_b},
    {'c',valid_c},
    {'d',valid_d},
    {'e',valid_e},
    {'m',valid_m},
    {'p',valid_p},
    {'x',valid_x},
    {'?',hlp_handle},
    {ERRCHAR,err_handle},
    {NULL,NULL}
};

// stringa di elenco delle opzioni; se una lettera e' seguita da ':' significa
// che l'opzione richiede un argomento

char *optionStr = "b:c:de:m:p:x?";

unsigned options;                                        // flags delle opzioni

// main() valida le opzioni via parseopt() e gestisce il ciclo di estrazione
// dei range per ogni riga di input. Tutti i messaggi del programma, incluso
// il copyright, sono scritti su stderr; in tal modo la redirezione
// dell'output prodotto non li include.

int main(int argc,char **argv)
{
    register int maxlen;
    register char *inibuf;
    char line[MAXLIN], buffer[MAXLIN];
    extern char *crgtMsg;
    extern char *eOptCut;
    extern unsigned options;
    extern char *optionStr;

    fprintf(stderr,crgtMsg,PRG,REL,YEAR);
    if(!parseopt(argc,argv,optionStr,SWITCH,NULL,NULL,vfuncs))
        err_handle(NULL,(int)sys_errlist[errno]);   // sys_errlist: errori std
    if(!(options & CUTOPT))
        err_handle(NULL,(int)eOptCut);
    inibuf = buffer;
    maxlen = MAXLIN;

// se specificata -b, begStr puo' essere copiata nel buffer fuori dal ciclo
// perche' comunque e' sempre la stessa per ogni riga

    if(options & BEGOPT) {
        strcpy(buffer,begStr);
        inibuf += strlen(begStr);    // nuovo indirizzo a cui copiare i range
        maxlen -= strlen(begStr);    // spazio residuo nel buffer
    }
    while(gets(line))
        cutstream(line,inibuf,buffer,maxlen);
    return(0);
}

// cutstream() estrae i range dalla riga di testo ricevuta come parametro e
// scrive la riga output su stdout. Si noti che il paramtero bufprint e'
// l'indirizzo originale del buffer: questo infatti deve essere stampato
// tutto anche se cutstream() ne lavora solo una parte (se c'e' begStr). E'
// evidente che in assenza di opzione -b, buffer e bufprint contengono lo
// stesso indirizzo.

void cutstream(char *line,char *buffer,char *bufprint,int maxlen)
{
    register i, j, k;
    int len, inc, start, stop, lim;
    extern char *eLinEnd;
    extern unsigned options;
    extern CUTTER *cutArr;
    extern char padStr[], midStr[], endStr[];

    j = 0;
    len = strlen(line);

// ciclo di estrazione di range, iterato su ogni riga per tutti gli elementi
// dell'array di strutture CUTTER: ciascuna, infatti, descrive un range.

    for(i = 0; cutArr[i].start; ) {

// calcolo dell'incremento di scansione della riga: se start e' < di stop si
// procede da sx a dx e percio' l'incremento e' +1; se start > stop allora si
// procede da dx a sx (a ritroso): l'incremento e' -1.

        inc = (start = cutArr[i].start) > (stop = cutArr[i].stop) ? -1 : 1;
        lim = 0;                                        // usato per il padding

// se uno degli estremi del range cade fuori dalla riga allora bisogna valutare
// che fare a seconda delle opzioni richieste

        if(start > len || stop > len)
            if(options & DISCRDOPT)
                return;                                 // -d: scartare la riga
            else
                if(options & EXITOPT)
                    err_handle(NULL,(int)eLinEnd);      // -x: fine programma
                else {

// se la riga deve essere comunque processata allora si controlla se entrambi
// gli estremi sono fuori dal range: in questo caso i caratteri di padding
// sono pari alla differenza tra gli estremi piu' uno.

                    if(start > len && stop > len) {
                        len = 0;
                        if(options & PADOPT)
                            lim = 1+(stop-start)*inc;
                    }

// se uno solo degli estremi e' fuori dalla riga, allora una parte del range
// deriva da caratteri della riga, la cui fine diviene uno dei due estremi del
// range stesso (start o stop, a seconda della direzione). Il numero di
// caratteri di padding e' pari al numero di caratteri non estraibili dalla
// riga, cioe' alla parte "scoperta" del range.

                    else {
                        if(options & PADOPT)
                            lim = max(start,stop)-len;
                        start = min(start,len);
                        stop = min(stop,len);
                    }
                }
        if(len)                       // si copia solo se la riga non e' vuota!

// il ciclo gestisce l'operazione di copia; l'applicazione a start di un
// incremento positivo o negativo a seconda dei casi permette di gestire
// entrambe le situazioni (da sx a dx o viceversa)

            for(--start, --stop; j < maxlen-1; start += inc) {
                buffer[j++] = line[start];
                if(start == stop)
                    break;                                    // fine del range
            }

// se e' richiesta -p si copia la stringa di padding nel buffer per il numero
// di caratteri ancora mancanti nel range. Se la stringa e' piu' lunga del
// necessario viene troncata; se e' piu' corta viene ripetuta sino a riempire
// tutto lo spazio.

        if(options & PADOPT)
            for(k = 0; j < maxlen-1 && lim; lim--) {
                buffer[j++] = padStr[k++];
                if(!padStr[k])
                    k = 0;
            }
        ++i;

// se e' specificata -m e ci sono ancora range da estrarre (l'ultimo elemento
// dell'array di struct CUTTER contiene NULL in entrambe i campi per segnalare
// la fine) allora si concatena al buffer la stringa midStr. Non si usa
// strcat() per essere sicuri di non andare oltre lo spazio rimanente nel
// buffer.

        if((options & MIDOPT) && cutArr[i].start)
            for(k = 0; j < maxlen-1 && midStr[k]; )
                buffer[j++] = midStr[k++];
        else

// se invece quello appena estratto e' l'ultimo range ed e' specificata -e
// si concatena al buffer la endStr.

            if((options & ENDOPT) && !cutArr[i].start)
                for(k = 0; j < maxlen-1 && endStr[k]; )
                    buffer[j++] = endStr[k++];
    }
    buffer[j] = NULL;                                                 // tappo!
    puts(bufprint);
}

Lanciando CUT con l'opzione ­? sulla riga di comando viene visualizzato un testo di aiuto, che ne riassume le principali caratteristiche ed opzioni.

Il comando FOR rivisitato: DOLIST

Veniamo a FILES.DAT (generato da CUT), ogni riga del quale, ai fini del nostro esempio, rappresenta un nome di file: ipotizziamo che su ciascuno di essi sia necessario ripetere la medesima elaborazione. L'interprete DOS implementa il comando FOR, che non è in grado di accettare una lista variabile di parametri: risulta quindi impossibile utilizzare FILES.DAT allo scopo. La utility DOLIST offre un rimedio: essa esegue una riga di comando, che viene considerata suddivisa in due parti, di cui la seconda opzionale, inserendo tra di esse la riga letta dallo standard input. Se da questo possono essere lette più righe, il comando viene iterato per ciascuna di esse.

La complessità del meccanismo è solo apparente: nell'ipotesi che l'elaborazione del nostro esempio consista nell'eseguire un secondo file batch il cui parametro sia ogni volta un diverso file, la procedura diviene la seguente:

:ATTESA
TIMEGONE 230000
IF ERRORLEVEL 1 GOTO ADESSO
GOTO ATTESA
:ADESSO
ECHO LE ORE 23:00 SONO APPENA TRASCORSE
DIR "D:\PROC\OUTPUT.DAT" | FIND " 0 " | EMPTYLVL
IF ERRORLEVEL 1 GOTO NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE 0 BYTES: UTILIZZO FCREATE...
DATECMD FCREATE 0 C:\PROC\OUT\@a@M@G.OUT
GOTO END
:NONZERO
ECHO IL FILE OUTPUT.DAT HA DIMENSIONE MAGGIORE DI 0 BYTES: UTILIZZO COPY...
DATECMD COPY D:\PROC\OUTPUT.DAT C:\PROC\OUT\@a@M@G.OUT
ECHO GENERAZIONE DEL FILE DI RIEPILOGO...
SELSTR -cb "* RIEPILOGO *" "* FINE *" < D:\PROC\OUTPUT.DAT > C:\PROC\RIEPILOG.DAT
IF ERRORLEVEL 255 GOTO SELERROR
IF ERRORLEVEL 1 GOTO POSTELAB
ECHO IL FILE OUTPUT.DAT NON CONTIENE LA SEZIONE DI RIEPILOGO
GOTO END
:SELERROR
ECHO ERRORE NELLA GENERAZIONE DEL RIEPILOGO
:POSTELAB
ECHO GENERAZIONE DELLA TABELLA DI RIEPILOGO...
TYPE C:\PROC\RIEPILOG.DAT FIND /V "* RIEPIL" | FIND /V "* FINE *" > C:\PROC\TR.TMP
ECHO TABELLA DI RIEPILOGO > C:\PROC\TR.TXT
ECHO. >> C:\PROC\TR.TXT
ECHO    DATA       IMPORTO       CODICE >> C:\PROC\TR.TXT
ECHO +-------------------------------------+ >> C:\PROC\TR.TXT
CUT -b"³ " -m" ³ " -e" ³" -c11-18 -c27-37 -c1-10 < C:\PROC\TR.TMP >> C:\PROC\TR.TXT
ECHO +-------------------------------------+ >> C:\PROC\TR.TMP
ECHO GENERAZIONE DELLA LISTA DI FILES...
CUT -c19-26 < C:\PROC\TR.TMP > C:\PROC\FILES.DAT
DEL C:\PROC\TR.TMP
ECHO ELABORAZIONE DELLA LISTA DI FILES IN FILES.DAT...
DOLIST "CALL ELABFILE.BAT" < FILES.LST
IF ERRORLEVEL GOTO FILERROR
GOTO END
:FILERROR
ECHO ERRORE NELLA ELABORAZIONE DELLA LISTA DI FILES
:END

DOLIST esegue la riga di comando una volta per ogni riga presente nel file FILES.DAT: con i dati dell'esempio, sono generati e lanciati i seguenti comandi:

CALL ELABFILE.BAT OPER0001
CALL ELABFILE.BAT OPER0034
CALL ELABFILE.BAT OPER1028
....
CALL ELABFILE.BAT OPER0017
CALL ELABFILE.BAT OPER0131
CALL ELABFILE.BAT OPER0007

Si noti l'utilizzo delle virgolette: esse sono necessarie in quanto CALL ed ELABFILE.BAT devono costituire insieme la prima parte del comando. Senza le virgolette DOLIST interpreterebbe CALL come prima porzione della command line e ELABFILE.BAT come seconda, inserendo tra esse la riga letta da FILES.DAT. I comandi eseguiti sarebbero perciò

CALL OPER0001 ELABFILE.BAT
CALL OPER0034 ELABFILE.BAT
CALL OPER1028 ELABFILE.BAT
....
CALL OPER0017 ELABFILE.BAT
CALL OPER0131 ELABFILE.BAT
CALL OPER0007 ELABFILE.BAT

con scarse possibilità di successo.

Segue il listato di DOLIST, ampiamente commentato.

/******************************************************************************

    DOLIST.C - Barninga_Z! - 17/10/93
    
    Esegue un comando dos su una lista di argomenti. Esempio
    
    dolist copy c:\backups\*.bak < files.lst
    
    copia tutti i files elencati nel file files.lst nella dir c:\backups
    attribuendo a ciascuno estensione .bak. Il files contenente gli
    argomenti deve essere in formato ascii, una riga per ogni comando.
    In pratica dolist compone ed esegue il comando:
    
    comando riga_della_lista comando_2a_parte

    In uscita ERRORLEVEL e' settato come segue:

    0 = comando eseguito (indipenentemente dal suo risultato)
    1 = elaborazione interrotta (errore di sintassi della cmdline di DOLIST)
    2 = comando non eseguito (system fallita)
    3 = comando non eseguito (command line da eseguire > 127 caratteri)
    
    Compilato sotto Borland C++ 3.1:
    
    bcc -d -rd -k- dolist.c
    
******************************************************************************/
#pragma  warn -pia

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

#define  PRG         "DOLIST"
#define  VER         "1.0"
#define  YEAR        "93"
#define  SEP_STR     " "
#define  NUL_CHRS    " \t\n\r"
#define  MAXCMD      128
#define  MAXLIN      256

// main() gestisce tutte le operazioni necessarie

int main(int argc,char **argv)
{
    register len, len2 = 0, retCode = 0;
    char *ptr;
    char cmd[MAXCMD], line[MAXLIN];

    printf("%s %s: esegue comando su lista argomenti - Barninga Z! '%s\n",PRG,
                                                                     VER,YEAR);
    if((argc < 2) || (argc > 3)) {
        printf("%s: sintassi: comando [comando_2a_parte] < lista_args\n",PRG);
        retCode = 1;
    }
    else {

// comincia la costruzione della command line copiando la prima parte del
// comando nel buffer appositamente predisposto

        strcpy(cmd,argv[1]);
        strcat(cmd,SEP_STR);
        len = strlen(cmd);
        if(argc == 3)
            len2 = strlen(argv[2])+strlen(SEP_STR);

// ciclo di elaborazione: per ogni riga del file specificato come stdin viene
// costruita una command line concatenando alla prima parte del comando
// (argv[1]) la riga letta dal file e la seconda parte del comando (argv[2])
// e viene eseguito il comando risultante via system()

        while(gets(line)) {
            if(ptr = strtok(line,NUL_CHRS)) {
                cmd[len] = NULL;
                if((len+len2+strlen(ptr)) < MAXCMD) {
                    strcat(cmd,ptr);
                    if(len2) {
                        strcat(cmd,SEP_STR);
                        strcat(cmd,argv[2]);
                    }
                    if(!system(cmd))                             // esecuzione!
                        printf("%s: eseguito: %s\n",PRG,cmd);
                    else {
                        printf("%s: fallito:  %s\n",PRG,cmd);
                        retCode = 2;
                    }
                }
                else {
                    printf("%s: ignorato: %s%s %s\n",PRG,cmd,ptr,argv[2]);
                    retCode = 3;
                }
            }
        }
    }
    return(retCode);
}

DOLIST restituisce in ERRORLEVEL un valore determinato dallo stato dell'elaborazione: in particolare, 0 indica che tutti i comandi sono stati eseguiti; 1 evidenza un errore di sintassi nella riga di comando; 2 indica il fallimento della chiamata alla funzione di libreria system(), che invoca l'interprete dei comandi DOS[11]; 3, infine, segnala che la lunghezza del comando composto mediante l'inserimento della riga di standard input tra prima e seconda parte della riga di comando risulta eccessiva[12].

I comandi nidificati: CMDSUBST

Che cosa accade in ELABFILE.BAT? Le ipotesi del nostro esempio si complicano sempre più (ma è l'ultimo sforzo, promesso!): la prima riga di ciascuno dei file elencati in FILES.DAT contiene un codice numerico, espresso in formato ASCII, che esprime uno stato relativo alle elaborazioni del riepilogo. La stringa 000 indica che quella particolare operazione è stata conclusa senza errori (e pertanto non vi sono altre righe nel file); tutti gli altri codici segnalano errori (il file contiene altre righe, descrittive dell'errore stesso). Scopo di POSTELAB.BAT è generare un file contenente le sole segnalazioni di errore.

Anche in questo caso il sistema Unix è fonte di ispirazione: la utility CMDSUBST consente di eseguire comandi DOS formati in tutto o in parte dall'output di altri comandi[13]. Vediamo subito un esempio: il comando

cmdsubst echo $type c:\autoexec.bat$

produce un output costituito dalla prima riga del file AUTOEXEC.BAT. Infatti, CMDSUBST scandisce la command line alla ricerca dei caratteri '$' [14] ed esegue, come un normale comando DOS, quanto tra essi compreso. La prima riga dello standard output prodotto dal comando è inserita nella command line originale, in luogo della stringa eseguita. Infine, è eseguita la command line risultante. Nell'esempio testè presentato, pertanto, viene dapprima lanciato il comando

type c:\autoexec.bat

Nell'ipotesi che la prima riga del file sia

@ECHO OFF

la command line risultante, eseguita da CMDSUBST, è

echo @ECHO OFF

la quale, a sua volta, produce lo standard output

@ECHO OFF

E' possibile specificare più subcomandi, ma non è possibile nidificarli: in altre parole, è valida una command line come:

cmdsubst echo $echo pippo$ $echo pluto$ $echo paperino$

che produce l'output

pippoplutopaperino

ma non lo è, o meglio, non è interpretata come forse ci si aspetterebbe, la seguente:

cmdsubst echo $echo $echo pippo$$

L'output prodotto è

echo is ON echo pippo$

Infatti, CMDSUBST individua il primo subcomando nella sequenza "$echo $", mentre la coppia "$$" è sostituita con un singolo carattere '$' (è questa, del resto, la sintassi che consente di specificare una command line contenente il '$' medesimo).

Torniamo al nostro caso pratico: il file ELABFILE.BAT può dunque essere strutturato come segue[15]:

@ECHO OFF
CMDSUBST IF @$TYPE D:\PROC\%1$@==@000@ GOTO END
IF ERRORLEVEL 1 GOTO ERRORE
SELSTR -f2 STRINGA_INESISTENTE < D:\PROC\%1 >> C:\PROC\ERRORI.DAT
GOTO END
:ERRORE
ECHO ERRORE NELL'ESECUZIONE DELL'ELABORAZIONE DI %1
:END

La sostituzione effettuata da CMDSUBST genera un test IF dalla sintassi lecita; SELTSR estrae dal file tutto il testo a partire dalla seconda riga (opzione ­f), nell'ipotesi che la stringa "STRINGA_INESISTENTE" non compaia mai nei file elaborati. La variabile %1 è sostituita dal DOS con il parametro passato a ELABFILE.BAT dal batch chiamante (nell'esempio si tratta del nome del file da elaborare). Si noti, infine, che nel batch chiamante è opportuno inserire, prima della riga che invoca DOLIST, un comando di cancellazione del file C:\PROC\ERRORI.DAT, dal momento che SELSTR vi scrive lo standard output in modalità di append (il testo è aggiunto in coda al file, se esistente; in caso contrario questo è creato e "allungato" di volta in volta).

Si ha perciò:

...
ECHO ELABORAZIONE DELLA LISTA DI FILES IN FILES.DAT...
IF EXIST C:\PROC\ERRORI.DAT DEL C:\PROC\ERRORI.DAT
DOLIST "CALL ELABFILE.BAT" < FILES.LST
IF ERRORLEVEL 1 GOTO FILERROR
...

Il listato di CMDSUBST, ampiamente commentato, è riportato di seguito.

/******************************************************************************

    CMDSUBST.C - Barninga Z! - 1994

    Esegue command line sostituendo la prima riga dello stdout di un altro
    comando compreso tra i caratteri $. Vedere stringa di help per i
    particolari.

    Compilato sotto Borland C++ 3.1

    bcc cmdsubst.c

******************************************************************************/
#pragma  warn -pia

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <alloc.h>
#include <dir.h>
#include <process.h>

#define  PRG           "CMDSUBST"
#define  VER           "1.0"
#define  YEAR          "'93"
#define  SUBST_FLAG    '$'
#define  MAXCMD        128
#define  TEMPLATE      "CSXXXXXX"
#define  ROOTDIR       "?:\\"
#define  OUTREDIR      ">"
#define  SEPSTR        " "
#define  EOL           '\n'

char *helpStr = "\
The command line is searched for '$' characters. If found, a string between two\n\
'$' is executed as a command line and the first line of its standard output\n\
replaces it in the CMDSUBST command line itself. Example: if the first line of\n\
the file c:\\autoexec.bat is\n\n\
@ECHO OFF\n\n\
the command line\n\n\
CMDSUBST echo $type c:\\autoexec.bat$\n\n\
causes the command\n\n\
echo @ECHO OFF\n\n\
to be actually executed. If a CMDSUBST command line contains a '$', it must be\n\
typed twice.\n\
";

// il template SUBCTL costituisce lo strumento per il controllo della
// sostituzione dei comandi nella command line. Il campo init referenzia
// il primo byte del comando, il campo len ne contiene la lunghezza. Viene
// allocato un array di strutture, ogni elemento del quale controlla la
// sostituzione di un comando.

typedef struct {
    char *init;
    int   len;
} SUBCTL;

int main(int argc,char **argv);
char *execSubCmd(char *subCmd,char *tempPath);
char *getCmdLine(int argc,char **argv);
char *getSubCmdOutput(char *outString,char *tempPath);
SUBCTL *initSubCtl(SUBCTL *ctl,int items,char *cmdBase);
char *makeCmdLine(SUBCTL *ctl,char *cmdLine,char *tempPath);
char *makeTempPath(char *tempPath);
SUBCTL *parseCmdLine(char *cmdLine);

// main() controlla l'esecuzione del programma in 4 fasi: ricostruzione
// della command line; creazione di un file temporaneo per la gestione
// dello stdout dei programmi; scansione della command line ed esecuzione
// dei subcomandi in essa contenuti; costruzione della nuova command line
// e sua esecuzione.

int main(int argc,char **argv)
{
    char *cmdLine;
    char tempPath[MAXPATH];
    SUBCTL *ctl;
    
    fprintf(stderr,"%s %s - Command line parms substitution - Barninga Z! %s\n",
                                                                  PRG,VER,YEAR);
    if(!(cmdLine = getCmdLine(argc,argv))) {
        fprintf(stderr,"%s: error: a command line must be supplied.\n%s",PRG,
                                                                      helpStr);
        return(1);
    }
    if(!makeTempPath(tempPath)) {
        fprintf(stderr,"%s: error: no more unique file names.\n",PRG);
        return(1);
    }
    if(!(ctl = parseCmdLine(cmdLine))) {
        fprintf(stderr,"%s: error: memory fault or malformed command.\n",PRG);
        return(1);
    }
    if(cmdLine = makeCmdLine(ctl,cmdLine,tempPath))
        system(cmdLine);
    return(0);
}

// execSubCmd() esegue un subcomando compreso nella command line di CMDSUBST.
// Viene costruita una command line contenente il subcomando e l'istruzione
// di redirezione del suo standard output nel file temporaneo il cui nome
// e' stato costruito da makeTempPath(). L'output e' poi analizzato dalla
// getSubCmdOutput() e il file temporaneo e' cancellato. Il sottocomando e'
// eseguito via system().

char *execSubCmd(char *subCmd,char *tempPath)
{
    char *ptr = NULL;
    static char outString[MAXCMD];
    
    if((strlen(subCmd)+strlen(tempPath)+strlen(OUTREDIR)) < MAXCMD) {
        strcat(subCmd,OUTREDIR);
        strcat(subCmd,tempPath);
        if(!system(subCmd))
            if(!getSubCmdOutput(outString,tempPath))
                fprintf(stderr,"%s: error: subcmd output not available.\n",PRG);
            else
                ptr = outString;
        else
            fprintf(stderr,"%s: error: subcmd exec failure.\n",PRG);
        unlink(tempPath);
    }
    else
        fprintf(stderr,"%s: error: subcmd string too long.\n",PRG);
    return(ptr);
}

// getCmdLine() ricostruisce la command line passata a CMDSUBST concatenando
// in un buffer static tutti gli elementi di argv. Il buffer deve essere
// static perche' l'indirizzo e' restituito alla funzione chiamante. In
// alternativa lo si potrebbe allocare con malloc()

char *getCmdLine(int argc,char **argv)
{
    register i;
    static char cmdLine[MAXCMD];

    if(argc == 1)
        return(NULL);
    cmdLine[0] = NULL;
    for(i = 1; argv[i]; i++) {
        strcat(cmdLine,argv[i]);
        strcat(cmdLine,SEPSTR);
    }
    cmdLine[strlen(cmdLine)-1] = NULL;
    return(cmdLine);
}

// getSubCmdOutput() apre il file temporaneo contenente lo standard output
// del subcomando eseguito e ne legge la prima riga, che deve essere
// sostituita al subcomando stesso nella command line di CMDSUBST.

char *getSubCmdOutput(char *outString,char *tempPath)
{
    char *ptr;
    FILE *subCmdOut;
    
    *outString = NULL;
    if(subCmdOut = fopen(tempPath,"r")) {
        if(!fgets(outString,MAXCMD,subCmdOut))
            *outString = NULL;
        else
            if(ptr = strrchr(outString,EOL))
                *ptr = NULL;
        fclose(subCmdOut);
    }
    return(outString);
}

// initSubCtl() inizializza una struttura di controllo del subcomando,
// riallocando l'array. La prima chiamata a initSubCtl() le passa un NULL
// come puntatore all'array, cosi' la funzione puo' usare realloc() anche
// per allocare la prima struttura.

SUBCTL *initSubCtl(SUBCTL *ctl,int items,char *cmdBase)
{
    if(!(ctl = (SUBCTL *)realloc(ctl,items*sizeof(SUBCTL)))) {
        fprintf(stderr,"%s: error: not enough memory.\n",PRG);
        return(NULL);
    }
    ctl[items-1].init = cmdBase+strlen(cmdBase);
    ctl[items-1].len = 0;
    return(ctl);
}

// makeCmdLine() costruisce la nuova command line che CMDSUBST deve eseguire
// sostituendo ai sottocomandi la prima riga del loro standard output. La
// command line cosi' costruita e' restituita a main(), che la esegue via
// system().

char *makeCmdLine(SUBCTL *ctl,char *cmdLine,char *tempPath)
{
    register i = 0, j;
    char *newPtr, *outString;
    char subCmd[MAXCMD];
    static char newCmdLine[MAXCMD];
    
    newPtr = newCmdLine;
    do {
        while(cmdLine < ctl[i].init-1)
            *newPtr++ = *cmdLine++;
        if(!(*ctl[i].init))
            *newPtr++ = *cmdLine;
        *newPtr = NULL;
        cmdLine += ctl[i].len+2;
        for(j = 0; j < ctl[i].len; j++)
            subCmd[j] = ctl[i].init[j];
        if(ctl[i].len) {
            subCmd[j] = NULL;
            if(!(outString = execSubCmd(subCmd,tempPath)))
                return(NULL);
            if(!(strlen(newCmdLine)+strlen(outString) < MAXCMD))
                return(NULL);
            strcat(newCmdLine,outString);
            newPtr = strchr(newCmdLine,NULL);
        }
    } while(ctl[i++].len);
    return(newCmdLine);
}

// makeTempPath() prepara un nome di file temporaneo completo di path formato
// dal drive di default e dalla root. Il file temporaneo e' percio' scritto
// in root ed e' destinato a contenere lo stdout del comando eseguito

char *makeTempPath(char *tempPath)
{
    strcpy(tempPath,ROOTDIR);
    strcat(tempPath,TEMPLATE);
    *tempPath = (char)getdisk()+'A';
    return(mktemp(tempPath+strlen(ROOTDIR)));
}

// parseCmdLine() scandisce la command line passata a CMDSUBST per individuare
// eventuali subcomandi racchiusi tra i '$'. Per ogni subcomando e' allocata
// una nuova struttura SUBCTL nell'array. Se sono incontrati due '$'
// consecutivi, si presume che essi rappresentino un solo '$' effettivo nella
// command line: uno e' scartato e non ha luogo alcuna sostituzione. La
// funzione non e' in grado di gestire sostituzioni nidificate.

SUBCTL *parseCmdLine(char *cmdLine)
{
    register i = 0, flag = 0;
    char *ptr, *cmdBase;
    SUBCTL *ctl;
    
    if(!(ctl = initSubCtl(NULL,1,cmdBase = cmdLine)))
        return(NULL);
    while(ptr = strchr(cmdLine,SUBST_FLAG)) {
        cmdLine = ptr+1;
        if(*cmdLine == SUBST_FLAG) 
            strcpy(cmdLine,cmdLine+1);
        else {
            flag = flag ? 0 : 1;
            if(!(*ctl[i].init)) 
                ctl[i].init = cmdLine;
            else {
                ctl[i].len = ptr-ctl[i].init;
                ++i;
                if(!(ctl = initSubCtl(ctl,i+1,cmdBase)))
                    return(NULL);
            }
        }
    }
    return(flag ? NULL : ctl);
}

Qualora non sia possibile effettuare la sostituzione nella command line (riga di comando risultante eccessivamente lunga o caratteri '$' scorrettamente appaiati), o la funzione system() restituisca un codice di errore[16], CMDSUBST termina con un valore di ERRORLEVEL diverso da 0.


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...