% This is ctie.w % Copyright (c) 2002,2003 by Julian Gilbey % All rights reserved. % % This program is distributed WITHOUT ANY WARRANTY, express or implied. % % Permission is granted to make and distribute verbatim copies of this % program provided that the copyright notice and this permission notice % are preserved on all copies. % % Permission is granted to copy and distribute modified versions of this % program under the conditions for verbatim copying, provided that the % entire resulting derived work is distributed under the terms of a % permission notice identical to this one. % % This program is based heavily on tie.w and common.w, part of the % CWEB source. \def\title{The CTIE processor} \def\botofcontents{% \vfill $\copyright$ 2002,2003 Julian Gilbey All rights reserved. This program is distributed WITHOUT ANY WARRANTY, express or implied. Permission is granted to make and distribute verbatim copies of this program provided that the copyright notice and this permission notice are preserved on all copies. Permission is granted to copy and distribute modified versions of this program under the conditions for verbatim copying, provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one. } @* Introduction. Whenever a programmer wants to change a given \.{WEB} or \.{CWEB} program (referred to as a \.{WEB} program throughout this program) because of system dependencies, she or he will create a new change file. In addition there may be a second change file to modify system independent modules of the program. But the \.{WEB} file cannot be tangled and weaved with more than one change file simultaneously. The \.{TIE} program was designed to merge a \.{WEB} file and several change files producing a new \.{WEB} file, and since the input files are tied together, the program was called \.{TIE}. Furthermore, the program could be used to merge several change files giving a new single change file. This method seems to be more important because it doesn't modify the original source file. However, the introduction of \.{CWEB} has meant that \.{TIE} is not quite able to perform its task correctly any longer: \.{CWEB} introduced the idea of include files, which are input into \.{CWEB} files using the \.{@@i} command, and \.{TIE} is unable to handle such constructs if the change files modify lines included in those files. The present program, \.{CTIE}, is designed to overcome this lack. Like \.{TIE}, upon which it is based, it can either output a single master \.{WEB} file or a master change file. However, in both cases, any include commands will be totally expanded and the files included in the output rather than the \.{@@i} commands being left; this makes this code feasible, which it would not necessarily be otherwise. Other than this difference, \.{CTIE} should function identically to \.{TIE} on files which do not involve any \.{CWEB} include commands. The algorithm used is essentially the same as that of \.{TIE}, with modifications to check for and handle \.{@@i} commands. Thus, as with \.{TIE}, the method used only needs one buffer line for each input file. Thus the storage requirement of \.{CTIE} does not depend on the sizes of the input files but only on their number. The program is written in \CEE/ and has few system dependencies. The ``banner line'' defined here should be changed whenever \.{CTIE} is modified. We also keep the version number here separately for ease; it is used below. @d version_number "1.1" @d banner "This is CTIE, Version 1.1" @d copyright "Copyright 2002,2003 Julian Gilbey. All rights reserved. There is no warranty.\n\ Run with the --version option for other important information." @ The main outline of the program is now given. This can be used more or less for any \CEE/ program. @c @@; @@; @@; @@; @@; @@; @@; @ And this is the structure of the |main| function: this is where \.{CTIE} starts, and where it ends. @= main(argc, argv) int argc; string *argv; { @; @@; @; @@; @@; @@; @@; @@; exit(wrap_up()); } @ We include the additional types |boolean| and |string|. \.{CTIE} replaces the complex \.{TIE} character set handling (based on that of the original \.{WEB} system) with the standard \.{CWEB} behaviour, and so uses the |char| type for input and output. @d false 0 @d true 1 @= typedef int boolean; typedef char* string; @ We predeclare some standard string-handling functions here instead of including their system header files, because the names of the header files are not as standard as the names of the functions. (There's confusion between \.{} and \.{}.) @= extern int strlen(); /* length of string */ extern char* strcpy(); /* copy one string to another */ extern int strncmp(); /* compare up to $n$ string characters */ extern char* strncpy(); /* copy up to $n$ string characters */ extern char *strerror(); @ The following parameters should be sufficient for most applications of \.{CTIE}. @^system dependencies@> @d buf_size 1024 /* maximum length of one input line */ @d max_file_index 32 /* we don't think that anyone needs more than 32 change files */ @d xisupper(c) (isupper(c)&&((unsigned char)c<0200)) @ We introduce a history variable that allows us to set a return code if the operating system can use it. First we introduce the coded values for the history. This variable must be initialized. (We do this even if the value given may be the default for variables, just to document the need for the initial value.) @d spotless 0 @d troublesome 1 @d fatal 2 @= int history=spotless; @* Input and output. Standard output for the user is done by writing on |stdout|. Error messages are written to |stderr|. Terminal input is not needed in this version of \.{CTIE}. |stdin|, |stdout| and |stderr| are predefined as we include the \.{stdio.h} definitions. @= #include @ And we need dynamic memory allocation. This should cause no trouble in any \CEE/ program. @^system dependencies@> @= #ifdef __STDC__ #include #else #include #endif @* Data structures. The multiple primary input files (master file and change files) are treated the same way. To organize the simultaneous usage of several input files, we introduce the data type \&{in\_file\_modes}. The mode |search| indicates that \.{CTIE} searches for a match of the input line with any line of an input file in |reading| mode. |test| is used whenever a match is found and it has to be tested if the next input lines do match also. |reading| describes that the lines can be read without any check for matching other lines. |ignore| denotes that the file cannot be used. This may happen because an error has been detected or because the end of the file has been found. \leavevmode |file_types| is used to describe whether a file is a master file or a change file. The value |unknown| is added to this type to set an initial mode for the output file. This enables us to check whether any option was used to select the kind of output. (this would even be necessary if we would assume a default action for missing options.) @= #define search 0 #define test 1 #define reading 2 #define ignore 3 typedef int in_file_modes; /* should be |enum(search, test, reading, ignore)| */ #define unknown 0 #define master 1 #define chf 2 typedef int file_types; /* should be |enum(unknown, master, chf)| */ @ A variable of type |out_md_type| will tell us in what state the output change file is during processing. |normal| will be the state, when we did not yet start a change, |pre| will be set when we write the lines to be changes and |post| will indicate that the replacement lines are written. @= #define normal 0 #define pre 1 #define post 2 typedef int out_md_type; /* should be |enum(normal, pre, post)| */ @ The next type will indicate variables used as an index into the file table. @= typedef int file_index; /* |-1..max_file_index+1| */ @ This is the data structure in which we collect information about each include file. @= typedef struct _indsc { char file_name[max_file_name_length]; long line; FILE *the_file; struct _indsc *parent; } include_description; @ The following data structure contains all of the information needed to use these input files. %`line' is a normal identifier throughout this program @f line dummy @= typedef struct _idsc{ string file_name; char buffer[buf_size]; in_file_modes mode; long line; file_types type_of_file; include_description *current_include; char *buffer_end; char *limit; char *loc; FILE *the_file; int dont_match; } input_description; @ Every one of the primary input files might include another file using the \.{@@i} include mechanism. In turn, each of these might include other files, and so on. We allow a limited number of these files to be opened simultaneously, and we store information about the currently open include files as a linked list attached to each primary file. @d max_include_files 20 /* maximum number of include files open simultaneously */ @d max_file_name_length 60 @= int total_include_files = 0; /* count 'em */ @ The following variables refer to the files in action, the number of change files, the mode of operation and the current output state. @= file_index actual_input, test_input, no_ch; file_types prod_chf=unknown; out_md_type out_mode; @ And the |actual_input| and |out_mode| variables need to be initialised sensibly. @= actual_input=0; out_mode=normal; @ All primary input files (including the master file) are recorded in the following structure. The components are usually accessed through a local pointer variable, requiring only a one-time-computation of the index expression. @= input_description *input_organisation[max_file_index+1]; @* File I/O. The basic function |get_line| can be used to get a line from an input file. The line is stored in the |buffer| part of the descriptor. The components |limit| and |line| are updated. If the end of the file is reached |mode| is set to |ignore|. On some systems it might be useful to replace tab characters by a proper number of spaces since several editors used to create change files insert tab characters into a source file not under control of the user. So it might be a problem to create a matching change file. @^tab character expansion@> We define |get_line| to read a line from a file specified by the corresponding file descriptor. This function returns |true| if it is successful and |false| if the end of the file has been reached. @= boolean get_line(i, do_includes) file_index i; boolean do_includes; { register input_description *inp_desc=input_organisation[i]; register FILE *fp; if (inp_desc->mode==ignore) return false; restart: if (inp_desc->current_include != NULL) { register include_description *inc_desc=inp_desc->current_include; fp=inc_desc->the_file; @@; } else { fp=inp_desc->the_file; @@; } if (do_includes) @@; return true; } @ Lines must fit into the buffer completely. We read all characters sequentially until an end of line is found (but do not forget to check for |EOF|!). Too long input lines will be truncated. This will result in a damaged output if they occur in the replacement part of a change file, or in an incomplerte check if the matching part is concerned. Tab character expansion might be done here. @^tab character expansion@> @= { register int c; /* the actual character read */ register char *k; /* where the next character goes */ if (feof(fp)) @@; inp_desc->limit = k = inp_desc->buffer; /* beginning of buffer */ while (k<=inp_desc->buffer_end && (c=getc(fp)) != EOF && c!='\n') if ((*(k++) = c) != ' ') inp_desc->limit = k; if (k>inp_desc->buffer_end) if ((c=getc(fp))!=EOF && c!='\n') { ungetc(c, fp); inp_desc->loc=inp_desc->buffer; err_print(i, "! Input line too long"); @.Input line too long@> } if (c==EOF && inp_desc->limit==inp_desc->buffer) @@; @@; } @ End of file is special if this file is the master file. Then we set the global flag variable |input_has_ended|. @= { inp_desc->mode=ignore; inp_desc->limit=NULL; /* mark end-of-file */ if (inp_desc->type_of_file==master) input_has_ended=true; fclose(fp); return false; } @ This variable must be declared for global access. @= boolean input_has_ended=false; @ This section does what its name says. Every 100 lines in the master file we print a dot, every 500 lines the number of lines is shown. @= inp_desc->line++; if (inp_desc->type_of_file==master && inp_desc->line % 100==0) { if (inp_desc->line % 500 == 0) printf("%ld", inp_desc->line); else putchar('.'); fflush(stdout); } @ The following is very similar to the above, but for the case where we are reading from an include file. @= { register int c; /* the actual character read */ register char *k; /* where the next character goes */ if (feof(fp)) @@; inp_desc->limit = k = inp_desc->buffer; /* beginning of buffer */ while (k<=inp_desc->buffer_end && (c=getc(fp)) != EOF && c!='\n') if ((*(k++) = c) != ' ') inp_desc->limit = k; if (k>inp_desc->buffer_end) if ((c=getc(fp))!=EOF && c!='\n') { ungetc(c, fp); inp_desc->loc=inp_desc->buffer; err_print(i, "! Input line too long"); @.Input line too long@> } if (c==EOF && inp_desc->limit==inp_desc->buffer) @@; inc_desc->line++; } @ We don't bail out if we find the end of an include file, we just return to the parent file. @= { include_description *temp=inc_desc->parent; fclose(fp); free(inc_desc); total_include_files--; inp_desc->current_include=temp; goto restart; } @ Usually, we have to check the line we have just read to see whether it begins with \.{@@i} and therefore needs expanding. @= { inp_desc->loc=inp_desc->buffer; *inp_desc->limit=' '; if (*inp_desc->buffer=='@@' && (inp_desc->buffer[1]=='i' || inp_desc->buffer[1]=='I')) { inp_desc->loc=inp_desc->buffer+2; *inp_desc->limit='"'; /* this will terminate the search in all cases */ while (*inp_desc->loc==' '||*inp_desc->loc=='\t') inp_desc->loc++; if (inp_desc->loc>=inp_desc->limit) { err_print(i, "! Include file name not given"); @.Include file name ...@> goto restart; } if (total_include_files>=max_include_files) { err_print(i, "! Too many nested includes"); @.Too many nested includes@> goto restart; } total_include_files++; /* push input stack */ @; } } @ When an \.{@@i} line is found in the file, we must temporarily stop reading it and start reading from the named include file. The \.{@@i} line should give a complete file name with or without double quotes. If the environment variable \.{CWEBINPUTS} is set, or if the compiler flag of the same name was defined at compile time, \.{CWEB} will look for include files in the directory thus named, if it cannot find them in the current directory. (Colon-separated paths are not supported.) The remainder of the \.{@@i} line after the file name is ignored. @d too_long() {total_include_files--; free(new_inc); err_print(i, "! Include file name too long"); goto restart;} @= { include_description *new_inc; char temp_file_name[max_file_name_length]; char *file_name_end; char *k, *kk; int l; /* length of file name */ new_inc=(include_description *) malloc(sizeof(include_description)); if (new_inc==NULL) fatal_error(i, "! No memory for new include descriptor", ""); new_inc->line=0; k=new_inc->file_name; file_name_end=k+max_file_name_length-1; if (*inp_desc->loc=='"') { inp_desc->loc++; while (*inp_desc->loc!='"' && k<=file_name_end) *k++=*inp_desc->loc++; if (inp_desc->loc==inp_desc->limit) k=file_name_end+1; /* unmatched quote is `too long' */ } else while (*inp_desc->loc!=' '&&*inp_desc->loc!='\t'&& *inp_desc->loc!='"'&&k<=file_name_end) *k++=*inp_desc->loc++; if (k>file_name_end) too_long(); @.Include file name ...@> *k='\0'; if ((new_inc->the_file=fopen(new_inc->file_name, "r"))!=NULL) { new_inc->parent=inp_desc->current_include; /* link it in */ inp_desc->current_include=new_inc; goto restart; /* success */ } kk=getenv("CWEBINPUTS"); if (kk!=NULL) { if ((l=strlen(kk))>max_file_name_length-2) too_long(); strcpy(temp_file_name, kk); } else { #ifdef CWEBINPUTS if ((l=strlen(CWEBINPUTS))>max_file_name_length-2) too_long(); strcpy(temp_file_name, CWEBINPUTS); #else l=0; #endif /* |CWEBINPUTS| */ } if (l>0) { if (k+l+2>=file_name_end) too_long(); for (; k>= new_inc->file_name; k--) *(k+l+1)=*k; strcpy(new_inc->file_name, temp_file_name); new_inc->file_name[l]='/'; /* \UNIX/ pathname separator */ if ((new_inc->the_file=fopen(new_inc->file_name, "r"))!=NULL) { new_inc->parent=inp_desc->current_include; /* link it in */ inp_desc->current_include=new_inc; goto restart; /* success */ } } total_include_files--; free(new_inc); err_print(i, "! Cannot open include file"); @.Cannot open include file@> goto restart; } @* Reporting errors to the user. There may be errors if a line in a given change file does not match a line in the master file or a replacement in a previous change file. Such errors are reported to the user by saying $$ \hbox{|err_print(file_no, "! Error message")|;} $$ where |file_no| is the number of the file which is concerned by the error. Please note that no trailing dot is supplied in the error message because it is appended by |err_print|. @= void err_print(); @ Here is the outline of the |err_print| function. @= void err_print(i, s) /* prints `\..' and location of error message */ file_index i; char *s; { char *k, *l; /* pointers into an appropriate |buffer| */ fprintf(stderr, *s=='!'? "\n%s" : "%s", s); if(i>=0) @@; else putc('\n', stderr); fflush(stderr); history=troublesome; } @ The error locations can be indicated by using the variables |loc|, |line| and |file_name| within the appropriate file description structures, which tell respectively the first unlooked-at position in the |buffer|, the current line number and the current file. We can determine whether we are looking at an included file or not by examining the |current_include| variable. This routine should be modified on systems whose standard text editor has special line-numbering conventions. @^system dependencies@> @= { register input_description *inp_desc=input_organisation[i]; register include_description *inc_desc=inp_desc->current_include; if (inc_desc!=NULL) { fprintf(stderr, ". (l. %ld of include file %s", inc_desc->line, inc_desc->file_name); fprintf(stderr, " included from l. %ld of %s file %s)\n", inp_desc->line, inp_desc->type_of_file==master?"master":"change", inp_desc->file_name); } else fprintf(stderr, ". (l. %ld of %s file %s)\n", inp_desc->line, inp_desc->type_of_file==master?"master":"change", inp_desc->file_name); l=(inp_desc->loc>=inp_desc->limit? inp_desc->limit: inp_desc->loc); if (l>inp_desc->buffer) { for (k=inp_desc->buffer; kbuffer; klimit; k++) putc(*k, stderr); /* print the part not yet read */ putc('\n', stderr); } @ Non recoverable errors are handled by calling |fatal_error| that outputs a message and then calls `|wrap_up|' and exits. |err_print| will print the error message followed by an indication of where the error was spotted in the source files. |fatal_error| cannot state any files because the problem is usually to access these. @d fatal_error(i, s, t) { fprintf(stderr, "\n%s", s); err_print(i, t); history=fatal; exit(wrap_up()); } @ Some implementations may wish to pass the |history| value to the operating system so that it can be used to govern whether or not other programs are started. Here, for instance, we pass the operating system a status of~0 if and only if only harmless messages were printed. @^system dependencies@> @= int wrap_up() { @; if (history > spotless) return 1; else return 0; } @ Always good to prototype. @= int wrap_up(); @ We report the history to the user, although this may not be ``\UNIX/'' style---but we are in good company: \.{WEB} and \TEX/ do the same. We put this on |stdout| rather than |stderr|, so that users can easily filter this away if they wish. @^system dependencies@> @= switch (history) { case spotless: printf("\n(No errors were found.)\n"); break; case troublesome: printf("\n(Pardon me, but I think I spotted something wrong.)\n"); break; case fatal: printf("(That was a fatal error, my friend.)\n"); } /* there are no other cases */ @ If there's a system error, we may be able to give the user more information with the |pfatal_error| function. This prints out system error information if it is available. @= void pfatal_error(); @ @= void pfatal_error(s, t) char *s, *t; { char *strerr=strerror(errno); fprintf(stderr, "\n%s%s", s, t); if (strerr) fprintf(stderr, " (%s)\n", strerr); else fprintf(stderr, "\n"); history=fatal; exit(wrap_up()); } @ We need an include file for the above. @= #include @* Handling multiple change files. In the standard version we take the name of the files from the command line. It is assumed that filenames can be used as given in the command line without changes. First there are some sections to open all files. If a file is not accessible, the run will be aborted. Otherwise the name of the open file will be displayed. @= { out_file=fopen(out_name, "w"); if (out_file==NULL) { pfatal_error("! Cannot open/create output file", ""); @.Cannot open/create output file@> } } @ The name of the file and the file desciptor are stored in global variables. @= FILE *out_file; string out_name; @ For the master file we start by reading its first line into the buffer, if we could open it. @= { input_organisation[0]->the_file= fopen(input_organisation[0]->file_name, "r"); if (input_organisation[0]->the_file==NULL) pfatal_error("! Cannot open master file ", input_organisation[0]->file_name); @.Cannot open master file@> printf("(%s)\n", input_organisation[0]->file_name); input_organisation[0]->type_of_file=master; get_line(0, true); } @ For the change files we must skip any comment part and see whether there are any changes in it. This is done by |init_change_file|. @= { file_index i; i=1; while (ithe_file= fopen(input_organisation[i]->file_name, "r"); if (input_organisation[i]->the_file==NULL) pfatal_error("! Cannot open change file ", input_organisation[i]->file_name); @.Cannot open change file@> printf("(%s)\n", input_organisation[i]->file_name); init_change_file(i); i++; } } @*Input/output organisation. Here's a simple function that checks if two lines are different. @= boolean lines_dont_match(i, j) file_index i, j; { register input_description *iptr=input_organisation[i], *jptr=input_organisation[j]; if (iptr->limit-iptr->buffer != jptr->limit-jptr->buffer) return true; return strncmp(iptr->buffer, jptr->buffer, iptr->limit-iptr->buffer); } @ Function |init_change_file(i)| is used to ignore all lines of the input file with index~|i| until the next change module is found. @= void init_change_file(i) file_index i; { register input_description *inp_desc=input_organisation[i]; char ccode; inp_desc->limit=inp_desc->buffer; @@; @@; inp_desc->dont_match=0; } @ While looking for a line that begins with \.{@@x} in the change file, we allow lines that begin with \.{@@}, as long as they don't begin with \.{@@y}, \.{@@z} or \.{@@i} (which would probably mean that the change file is fouled up). @= while(1) { if (!get_line(i, false)) return; /* end of file reached */ if (inp_desc->limitbuffer+2) continue; if (inp_desc->buffer[0]!='@@') continue; ccode=inp_desc->buffer[1]; if (xisupper(ccode)) ccode=tolower(ccode); if (ccode=='x') break; if (ccode=='y' || ccode=='z' || ccode=='i') { inp_desc->loc=inp_desc->buffer+2; err_print(i, "! Missing @@x in change file"); @.Missing @@x...@> } } @ Here we are looking at lines following the \.{@@x}. @= do { if (!get_line(i, true)) { err_print(i, "! Change file ended after @@x"); @.Change file ended...@> return; } } while (inp_desc->limit==inp_desc->buffer); @ The |put_line| function is used to write a line from input buffer |j| to the output file. @= void put_line(j) file_index j; { char *ptr=input_organisation[j]->buffer; char *lmt=input_organisation[j]->limit; while(ptr= boolean e_of_ch_module(i) file_index i; { register input_description *inp_desc=input_organisation[i]; if (inp_desc->limit==NULL) { err_print(i, "! Change file ended without @@z"); @.Change file ended without @@z@> return true; } else if (inp_desc->limit>=inp_desc->buffer+2) if (inp_desc->buffer[0]=='@@' && (inp_desc->buffer[1]=='Z' || inp_desc->buffer[1]=='z')) return true; return false; } @ The function |e_of_ch_preamble| returns |true| if the input line from file~|i| starts with \.{@@y}. @= boolean e_of_ch_preamble(i) file_index i; { register input_description *inp_desc=input_organisation[i]; if (inp_desc->limit>=inp_desc->buffer+2 && inp_desc->buffer[0]=='@@') if (inp_desc->buffer[1]=='Y'||inp_desc->buffer[1]=='y') { if (inp_desc->dont_match>0) { inp_desc->loc=inp_desc->buffer+2; fprintf(stderr, "\n! Hmm... %d ", inp_desc->dont_match); err_print(i, "of the preceding lines failed to match"); } return true; } return false; } @ To process the input file the next section reads a line of the current (actual) input file and updates the |input_organisation| for all files with index greater than |actual_input|. @= { file_index test_file; @@; if (input_has_ended && actual_input==0) break; /* all done */ @@; @@; @@; } @ Any of the current change files may have reached the end of the current change. In such a case, intermediate lines must be skipped and the next start of change is to be found. This may make a change file become inactive if the end of the file is reached. @= { register input_description *inp_desc; while (actual_input>0 && e_of_ch_module(actual_input)) { inp_desc=input_organisation[actual_input]; if (inp_desc->type_of_file==master) { /* emergency exit, everything mixed up!*/ fatal_error(-1, "! This can't happen: change file is master file", ""); @.This can't happen...@> } inp_desc->mode=search; init_change_file(actual_input); while ((input_organisation[actual_input]->mode!=reading && actual_input>0)) actual_input--; } } @ Now we will set |test_input| to the first change file that is being tested against the current line. If no other file is testing, then |actual_input| refers to a line to write and |test_input| is set to |none|. @d none (-1) @= test_input=none; test_file=actual_input; while (test_input==none && test_filemode) { case search: if (lines_dont_match(actual_input, test_file)==false) { input_organisation[test_file]->mode=test; test_input=test_file; } break; case test: if (lines_dont_match(actual_input, test_file)) { /* error, sections do not match; just note at this point */ input_organisation[test_file]->dont_match++; } test_input=test_file; break; case reading: /* this can't happen */ break; case ignore: /* nothing to do */ break; } } @ For the output we must distinguish between whether we are creating a new change file or a new master file. Change file creation requires closer inspection because we may be before a change, in the pattern (match) part or in the replacement part. For master file creation, we simply have to write the line from the current (actual) input. @= if (prod_chf==chf) { while(1) { @@; @@; @@; } } else if (test_input==none) put_line(actual_input); @ Check whether we have to start a change file entry. Without a match nothing needs to be done. @= if (out_mode==normal) { if (test_input!=none) { fprintf(out_file, "@@x\n"); out_mode=pre; } else break; } @ Check whether we have to start the replacement text. This is the case when we are in |pre| mode but have no more matching lines. Otherwise the master file source line must be copied to the change file. @= if (out_mode==pre) { if (test_input==none) { fprintf(out_file, "@@y\n"); out_mode=post; } else { if (input_organisation[actual_input]->type_of_file==master) put_line(actual_input); break; } } @ Check whether an entry from a change file is complete. If the current input is from a change file which is not being tested against a later change file, then this change file line must be written. If the actual input has been reset to the master file, we can finish this change. @= if (out_mode==post) { if (input_organisation[actual_input]->type_of_file==chf) { if (test_input==none) put_line(actual_input); break; } else { fprintf(out_file, "@@z\n\n"); out_mode=normal; } } @ If we had a change, we must proceed in the actual file to be changed and in the change file in effect. @= get_line(actual_input, true); if (test_input!=none) { get_line(test_input, true); if (e_of_ch_preamble(test_input)==true) { get_line(test_input, true); /* update current changing file */ input_organisation[test_input]->mode=reading; actual_input=test_input; test_input=none; } } @ To create the new output file we have to scan the whole master file and all changes in effect when it ends. At the very end it is wise to check for all changes to have completed, in case the last line of the master file was to be changed. @= actual_input=0; input_has_ended=false; while (input_has_ended==false||actual_input!=0) @@; if (out_mode==post) /* last line has been changed */ fprintf(out_file, "@@z\n"); @ At the end of the program, we will tell the user if the change file had a line that didn't match any relevant line in the master file or any of the change files. @= { file_index i; for (i=1;imode!=ignore) { input_organisation[i]->loc=input_organisation[i]->buffer; err_print(i, "! Change file entry did not match"); @.Change file entry ...@> } } } @ We want to tell the user about our command line options if they made a mistake. This is done by the |usage_error()| function. It contains merely the necessary print statements and exits afterwards. @= void usage_error() { @; fprintf(stderr, "Usage: ctie -[mc] outfile master changefile(s)\n"); fprintf(stderr, "Type ctie --help for more information\n"); exit(1); } @ Printing our welcome banners; we only do this if we are not asked for version or help information. @= printf("%s\n", banner); /* print a ``banner line'' */ printf("%s\n", copyright); /* include the copyright notice */ @ We must scan through the list of parameters, given in |argv|. The number is in |argc|. We must pay attention to the flag parameter. We need at least 3~parameters (\.{-m} or \.{-c}, an output file and a master file) and can handle up to |max_file_index| change files. The names of the file parameters will be inserted into the structure of |input_organisation|. The first file is special. It indicates the output file. When we allow flags at any position, we must find out which name is for what purpose. The master file is already part of the |input_organisation| structure (index~0). As long as the number of files found (counted in |no_ch|) is |-1| we have not yet found the output file name. @= { if (argc>max_file_index+5-1) usage_error(); no_ch = -1; /* fill this part of |input_organisation| */ while (--argc>0) { argv++; if (strcmp("-help", *argv)==0 || strcmp("--help", *argv)==0) @; if (strcmp("-version", *argv)==0 || strcmp("--version", *argv)==0) @; if (**argv=='-') @@; else @@; } if (no_ch<=0|| prod_chf==unknown) usage_error(); } @ The flag is about to determine the processing mode. We must make sure that this flag has not been set before. Further flags might be introduced to avoid/force overwriting of output files. Currently we just have to set the processing flag properly. @= if (prod_chf!=unknown) usage_error(); else switch (*(*argv+1)) { case 'c': case 'C': prod_chf=chf; break; case 'm': case 'M': prod_chf=master; break; default: usage_error(); } @ We have to distinguish whether this is the very first file name (which is the case if |no_ch==(-1)|) or if the next element of |input_organisation| must be filled. @= { if (no_ch==(-1)) { out_name=*argv; } else { register input_description *inp_desc; inp_desc=(input_description *) malloc(sizeof(input_description)); if (inp_desc==NULL) fatal_error(-1, "! No memory for input descriptor", ""); @.No memory for descriptor@> inp_desc->mode=search; inp_desc->line=0; inp_desc->type_of_file=chf; inp_desc->limit=inp_desc->buffer; inp_desc->buffer[0]=' '; inp_desc->loc=inp_desc->buffer+1; inp_desc->buffer_end=inp_desc->buffer+buf_size-2; inp_desc->file_name=*argv; inp_desc->current_include=NULL; input_organisation[no_ch]=inp_desc; } no_ch++; } @ Modules for dealing with help messages and version info. We follow the \.{kpathsea} standard code here, so that we can easily adapt this to work with \.{kpathsea}. @= usage_help(); @.--help@> @ @= { print_version_and_exit("CTIE", version_number); @.--version@> } @ Here is the usage information for \.{--help}. @= string CTIEHELP[] = { "Usage: ctie -[mc] outfile master changefile(s)", " Create a new master file or change file from the given", " master (C)WEB file and changefiles.", " All filenames are taken literally; no suffixes are added.", "", "-m create a new master file from original (C)WEB and change file(s)", "-c create a master change file for original (C)WEB file from changefile(s)", "--help display this help and exit", "--version display version information and exit", NULL }; @ @= void usage_help(); void print_version_and_exit(); @ @c void usage_help() { string *message=CTIEHELP; while (*message) { fputs(*message, stdout); putchar('\n'); ++message; } putchar('\n'); exit(0); } @ @c void print_version_and_exit(name, version) string name, version; { printf ("%s %s\n", name, version); puts ("Copyright (C) 2002,2003 Julian Gilbey."); puts ("There is NO warranty. This is free software. See the source"); puts ("code of CTIE for redistribution conditions."); exit (0); } @* System-dependent changes. This section should be replaced, if necessary, by changes to the program that are necessary to make \.{CTIE} work at a particular installation. It is usually best to design your change file so that all changes to previous modules preserve the module numbering; then everybody's version will be consistent with the printed program. More extensive changes, which introduce new modules, can be inserted here; then only the index itself will get a new module number. @^system dependencies@> @* Index.