/* REXX */ /* */ G.!Id = '$Id$' /* * * :mode=netrexx:tabSize=4:indentSize=4:noTabs=true: * :folding=explicit:collapseFolds=1: * * ---------------------------------------------------------------------------- * * EMXEXP Wrapper * * Manages exports created by the emxexp tool from the GCC for OS/2 package. * * Please run the script without arguments to get some help. * * Author: Dmitry A. Kuminov * * FREEWARE. NO ANY WARRANTY..., ON YOUR OWN RISC... ETC. * */ signal on syntax signal on halt signal on novalue trace off numeric digits 12 '@echo off' /* globals ******************************************************************************/ G.!Title = 'emxexpw r.${revision} [${revision.date} ${revision.time}] - EMXEXP Wrapper' G.!tab = '09'x G.!eol = '0D0A'x G.!DefaultTemplate.1 =, 'LIBRARY ' /* <-- ${name} goes here */ G.!DefaultTemplate.2 =, ' INITINSTANCE TERMINSTANCE' G.!eol||, 'DATA MULTIPLE NONSHARED' G.!eol||, 'EXPORTS' /* <-- ${exports} go here */ G.!StatsTemplate =, G.!eol||, ';'G.!eol||, '; EXPORTS STATISTICS'G.!eol||, ';'G.!eol||, '; Total exports : %1'G.!eol||, '; Total weak (marked as weak) : %2'G.!eol||, '; Total new (marked as new) : %3'G.!eol||, '; Total resurrected (marked as renew) : %4'G.!eol||, '; Total outdated (marked as ;@@@) : %5'G.!eol||, ';' /* options */ Opt.!DefFile = '' Opt.!LibName = '' Opt.!LibVersion = '' Opt.!DefTemplate = '' Opt.!DefMap = '' Opt.!Quiet = 0 Opt.!Objects.0 = 0 /* all globals to be exposed in procedures */ Globals = 'G. Opt.' /* init rexx lib ******************************************************************************/ if (RxFuncQuery('SysLoadFuncs')) then do call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs' call SysLoadFuncs end /* startup code ******************************************************************************/ parse source . . G.!SourceFile parse value strip(G.!Id,,'$') with, 'Id: '.' 'G.!Revision' 'G.!RevisionDate' 'G.!RevisionTime' '. G.!Title = Replace(G.!Title, '${revision}', G.!Revision) G.!Title = Replace(G.!Title, '${revision.date}', G.!RevisionDate) G.!Title = Replace(G.!Title, '${revision.time}', G.!RevisionTime) parse arg args if (args == '') then call Usage call ParseCommandLine args /* check options */ if (Opt.!DefFile == '') then call Error E_NoDefFile if (Opt.!Objects.0 == 0) then call Error E_NoObjectFiles /* apply default options */ if (Opt.!LibName == '') then do parse value filespec('N', Opt.!DefFile) with Opt.!LibName '.' . end /* check emxexp.exe existence */ G.emxexp_exe = SysSearchPath('PATH', 'EMXEXP.EXE') if (G.emxexp_exe == '') then call Error E_CantFindEMXEXP Opt.!ObjectsList = '' do i = 1 to Opt.!Objects.0 Opt.!ObjectsList = Opt.!ObjectsList Opt.!Objects.i /* command line length limit (1024 chars) */ if (length(Opt.!ObjectsList) > 900) then do Opt.!ObjectsList = '' leave end end if (Opt.!ObjectsList \== '') then drop Opt.!Objects. call Main /* termination code ******************************************************************************/ call Done 0 /* functions ******************************************************************************/ /** * Just do the job. */ Main: procedure expose (Globals) rc = SysFileDelete(Opt.!DefFile) if (rc \== 0 & rc \== 2) then call Error E_CantDeleteFile, Opt.!DefFile, rc /* write object files to a responce file for EMXEXP */ if (Opt.!ObjectsList == '') then do G.!ObjectListFile = SysTempFileName(GetTempDir()'\emxxexp_?????.tmp') do i = 1 to Opt.!Objects.0 call lineout G.!ObjectListFile, Opt.!Objects.i end call lineout G.!ObjectListFile end else G.!ObjectListFile = '' if (Opt.!DefMap == '') then do /* * Easy path: no map file. Just use emxexp to generate the EXPORTS * section of the DEF file. */ have_exports_macro = 0 exports_tail = '' /* write stuff before ${exports} */ if (Opt.!DefTemplate = '') then do call lineout Opt.!DefFile,, G.!DefaultTemplate.1||Opt.!LibName||G.!DefaultTemplate.2 have_exports_macro = 1 end else do parse value WriteTemplatedDEF('B') with have_exports_macro' 'exports_tail end call lineout Opt.!DefFile if (have_exports_macro) then do if (Opt.!ObjectsList == '') then do say G.emxexp_exe '@'G.!ObjectListFile '>>'Opt.!DefFile G.emxexp_exe '@'G.!ObjectListFile '>>'Opt.!DefFile end else do say G.emxexp_exe Opt.!ObjectsList '>>'Opt.!DefFile G.emxexp_exe Opt.!ObjectsList '>>'Opt.!DefFile end call lineout Opt.!DefFile if (G.!ObjectListFile \= '') then do call SysFileDelete G.!ObjectListFile G.!ObjectListFile = '' end if (rc \== 0) then call Error E_EMXEXPFailed, rc end /* write remaining stuff after ${exports} */ if (Opt.!DefTemplate == '') then nop else do if (have_exports_macro) then call WriteTemplatedDEF 'A', exports_tail end end else do /* * Map file is given. Parse it and then combine with the temporary * exports generated by emxexp. */ /* parse the map file */ call ParseExportsSection Opt.!DefMap, 'P' /* call the original emxexp to put EXPORTS to a temporary DEF file */ G.!TempDefFile = SysTempFileName(GetTempDir()'\emxxexp_?????_def.tmp') if (Opt.!ObjectsList == '') then G.emxexp_exe '@'G.!ObjectListFile '>'G.!TempDefFile else G.emxexp_exe Opt.!ObjectsList '>'G.!TempDefFile if (rc \== 0) then call Error E_EMXEXPFailed, rc if (G.!ObjectListFile \= '') then do call SysFileDelete G.!ObjectListFile G.!ObjectListFile = '' end /* parse the temporary DEF file and combine with G.!Exports */ call ParseExportsSection G.!TempDefFile, 'C' call SysFileDelete G.!TempDefFile if (G.!Exports.0 == 0) then call Error E_NoExports /* update the map file */ rc = SysFileDelete(Opt.!DefMap) if (rc \== 0 & rc \== 2) then call Error E_CantDeleteFile, Opt.!DefMap, rc stats = AppendExportsToFile(Opt.!DefMap) /* returned stats are 'weak new renew coutdated' */ stats = ReplaceArgs(G.!StatsTemplate,, G.!Exports.0,, word(stats, 1),, word(stats, 2),, word(stats, 3),, word(stats, 4)) call lineout Opt.!DefMap, stats call lineout Opt.!DefMap /* generate a DEF file */ have_exports_macro = 0 exports_tail = '' /* write stuff before ${exports} */ if (Opt.!DefTemplate == '') then do call lineout Opt.!DefFile,, G.!DefaultTemplate.1||Opt.!LibName||G.!DefaultTemplate.2 have_exports_macro = 1 end else do parse value WriteTemplatedDEF('B') with have_exports_macro' 'exports_tail end call lineout Opt.!DefFile if (have_exports_macro) then do address 'cmd' 'copy "'Opt.!DefFile'" /B + "'Opt.!DefMap'" /B '||, '"'Opt.!DefFile'" 1>nul 2>nul' end /* write remaining stuff after ${exports} */ if (Opt.!DefTemplate == '') then nop else do if (have_exports_macro) then call WriteTemplatedDEF 'A', exports_tail end call lineout Opt.!DefFile end return /** * Expands DEF template macros and writes results to a DEF file. * * @param mode * 'B' to expand and write everything before '${exports}' or * 'A' (or any other string) to write what's left after it. * @param exports_tail * When mode is 'A', this is the string that was returned when calling * this procedure in 'B' mode. * When mode is 'B', this argument is not used. * @return * When mode is 'B': * '1 ' if '${exports}' has been found (where is the rest of * the line after '${exports}') or * '0' if no '${exports}' have been found (and no need to call this * procedure in 'A' mode). * When mode is 'A': * nothing. */ WriteTemplatedDEF: procedure expose (Globals) parse arg mode, exports_tail if (mode == 'B') then do str = '' epos = 0 parse value stream(Opt.!DefTemplate, 'C', 'OPEN READ') with status':'code if (status \== 'READY') then call Error E_CantOpenFile, Opt.!DefTemplate, code do while lines(Opt.!DefTemplate) str = linein(Opt.!DefTemplate) epos = pos('${exports}', str) if (epos == 0) then do str = Replace(str, '${name}', Opt.!LibName) str = Replace(str, '${version}', Opt.!LibVersion) call lineout Opt.!DefFile, str end else do call charout Opt.!DefFile, substr(str, 1, epos - 1) str = substr(str, epos + length('${exports}')) leave end end if (epos > 0) then return 1' 'str return 0 end call lineout Opt.!DefFile, exports_tail do while lines(Opt.!DefTemplate) str = linein(Opt.!DefTemplate) str = Replace(str, '${name}', Opt.!LibName) str = Replace(str, '${version}', Opt.!LibVersion) call lineout Opt.!DefFile, str end return /** * Parses the EXPORTS-like sectoion of given map or DEF file * and accumulate the results in the G.!Exports stem. * Parsing is based on the format of the EXPORTS section, generated * by emxexp. * * When mode is 'P'arse, exports are parsed and stored in the stem. * The given file must be a map file. Its format is similar to what * is generated by emxexp but has some incompatible additions. * First, the symbol name must be followed by an ordinal and the NONAME * keyword. Second, magicseg can appear on a separate comment line right * before a group of symbols it should be applied to (this can reduce the size * of the map file because there is no need to put magicseg in the comments * of every line that defines a symbol). * * When mode is 'C'ombine, exports being parsed are searched in the stem * and new symbols are added only if missing. Outdated symbols are also * specially marked. The format of the given file must be the same as * generated by emxexp. * * @note * The procedure doesn't currently check for duplicate ordinals! * (this will not happen until ordinals are manually hacked) * * @param file file to parse * @param mode 'P' to parse or 'C' (or any other string) to combine */ ParseExportsSection: procedure expose (Globals) parse arg file, mode if (mode \== 'P') then mode = 'C' if (mode == 'P') then do drop G.!Exports. G.!Exports.0 = 0 /* # of exports */ G.!Exports.!file = file G.!Exports.!maxOrd = 0 end ln = 0 global_magicseg = '' do while lines(file) ln = ln + 1 str = linein(file) call TokenizeString str, 'G.!line' if (G.!line.0 == 0) then iterate w = 1 outdated = 0 if (mode == 'P') then do if (G.!line.w == ';@@@') then do /* skip the magic 'outdated symbol' mark */ w = w + 1 outdated = 1 end else if (G.!line.w == ';') then do /* test for a global magicseg definition */ w = w + 1 if (left(G.!line.w, 10) == "magicseg='" &, right(G.!line.w, 1) == "'") then do global_magicseg = substr(G.!line.w, 11, length(G.!line.w) - 11) w = w + 1 end else global_magicseg = '' iterate end end if (G.!line.0 < w | left(G.!line.w, 1) == ';') then iterate symbol = G.!line.w if (symbol = '_DLL_InitTerm') then iterate w = w + 1 ordinal = '' if (mode == 'P') then do if (G.!line.0 >= w) then parse value(G.!line.w) with '@'ordinal if (\datatype(ordinal, 'W') | ordinal <= 0) then call Error E_InvalidOrdinal, symbol, ordinal, file, ln w = w + 1 if (G.!line.0 < w | translate(G.!line.w) \== 'NONAME') then call Error E_NoNoname, symbol, file, ln w = w + 1 end magicseg = '' weak = 0 if (G.!line.0 >= w + 1) then do if (G.!line.w == ';') then do w = w + 1 if (left(G.!line.w, 10) == "magicseg='" &, right(G.!line.w, 1) == "'") then do magicseg = substr(G.!line.w, 11, length(G.!line.w) - 11) w = w + 1 end do i = w to G.!line.0 if (G.!line.i == 'weak') then do weak = 1 leave end end end end if (magicseg == '') then magicseg = global_magicseg select when (magicseg == 'BSS32' | magicseg == 'DATA32') then nop when (magicseg == 'TEXT32') then nop otherwise call Error E_InvalidMagicSeg, symbol, magicseg, file, ln end if (mode == 'P') then do n = G.!Exports.0 + 1 G.!Exports.0 = n G.!Exports.n = symbol G.!Exports.n.!seg = magicseg G.!Exports.n.!weak = weak G.!Exports.n.!ord = ordinal if (outdated) then G.!Exports.n.!type = 'OO' /* mark as twice outdated */ else G.!Exports.n.!type = 'O' /* mark everything else as outdated */ signal off novalue o = G.!Exports.symbol signal on novalue if (datatype(o, 'W')) then call Error E_DuplicateSymbol, symbol, file, ln G.!Exports.symbol = n /* back reference! */ if (G.!Exports.!maxOrd < ordinal) then G.!Exports.!maxOrd = ordinal end else do signal off novalue n = G.!Exports.symbol signal on novalue if (datatype(n, 'W')) then do /* existing symbol */ if (n > 0 & n <= G.!Exports.0) then do if (G.!Exports.n.!type == 'OO') then G.!Exports.n.!type = 'R' /* mark as resurrected */ else G.!Exports.n.!type = '' /* remove outdated mark */ end else signal Nonsense end else do /* new symbol */ ordinal = G.!Exports.!maxOrd + 1 G.!Exports.!maxOrd = ordinal n = G.!Exports.0 + 1 G.!Exports.0 = n G.!Exports.n = symbol G.!Exports.n.!seg = magicseg G.!Exports.n.!weak = weak G.!Exports.n.!ord = ordinal G.!Exports.n.!type = 'N' /* mark all as new */ /* no need in back reference */ end end end call lineout file return /** * Appends exports from the G.!Exports stem to the existing file. * The generated contents corresponds to the format of the map file * described in ParseExportsSection. * * @param file file to append to */ AppendExportsToFile: procedure expose (Globals) parse arg file weak = 0 new = 0 renew = 0 outdated = 0 magicseg = '' do n = 1 to G.!Exports.0 prefix = ' ' suffix = '' if (G.!Exports.n.!weak) then do suffix = suffix' weak' weak = weak + 1 end if (G.!Exports.n.!type == 'N') then do suffix = suffix' new' new = new + 1 end else if (G.!Exports.n.!type == 'R') then do suffix = suffix' renew' renew = renew + 1 end else if (G.!Exports.n.!type == 'O' |, G.!Exports.n.!type == 'OO') then do prefix = prefix';@@@ ' outdated = outdated + 1 end seg = G.!Exports.n.!seg if (magicseg \= seg) then do magicseg = '' /* look at next 2 exports to see what magicseg do they have */ if (n + 2 <= G.!Exports.0) then do if (value('G.!Exports.'||(n + 1)||'.!seg') == seg &, value('G.!Exports.'||(n + 2)||'.!seg') == seg) then do magicseg = seg call lineout file, " ; magicseg='"magicseg"'" end end end if (magicseg \== '') then do if (suffix \== '') then suffix = ' ;'suffix end else suffix = " ; magicseg='"G.!Exports.n.!seg"'"suffix call lineout file,, prefix||G.!Exports.n '@'||G.!Exports.n.!ord 'NONAME'||, suffix end return weak' 'new' 'renew' 'outdated /** * Parses the command line and sets up the corresponding global * variables. * * @param string the command line to parse */ ParseCommandLine: procedure expose (Globals) parse arg options call TokenizeString options, 'G.!opts' /* first pass: read responce files */ resp_options = '' do i = 1 to G.!opts.0 if (left(G.!opts.i, 1) == '@') then do o = substr(G.!opts.i, 2) if (stream(o, 'C', 'QUERY EXISTS') == '') then call Error E_FileNotFound, o resp_options = resp_options || charin(o,, chars(o)) end end if (resp_options \== '') then do /* retokenize options */ call TokenizeString resp_options || options, 'G.!opts' end /* add an empty word after the last option */ call value 'G.!opts.'||(G.!opts.0 + 1), '' /* second pass: read direct options */ do i = 1 to G.!opts.0 if (left(G.!opts.i, 1) == '@') then iterate if (left(G.!opts.i, 1) == '-') then do o = substr(G.!opts.i, 2) select when (wordpos(o, 'h H ? help') > 0) then call Usage when (o == 'rawhelp') then call Usage 1 when (o == 'def') then do i = i + 1 Opt.!DefFile = G.!opts.i end when (o == 'name') then do i = i + 1 Opt.!LibName = G.!opts.i end when (o == 'version') then do i = i + 1 Opt.!LibVersion = G.!opts.i end when (o == 'template') then do i = i + 1 Opt.!DefTemplate = G.!opts.i end when (o == 'map') then do i = i + 1 Opt.!DefMap = G.!opts.i end when (o == 'q') then do Opt.!Quiet = 1 end otherwise call Error E_InvalidOption, o end end else do n = Opt.!Objects.0 + 1 Opt.!Objects.n = G.!opts.i Opt.!Objects.0 = n end end /* drop the contents of our global stem */ do i = 1 to G.!opts.0 drop G.!opts.i end drop G.!opts.0 return /** * Prints the usage information. */ Usage: procedure expose (Globals) parse arg just_say just_say = (just_say == 1) if (\just_say) then do address 'cmd' 'call' G.!SourceFile '-rawhelp | more' call Done 0 end say G.!Title say say 'Usage: 'filespec('N', G.!SourceFile)' ... [@...]' say say 'Options:' say say ' -def : Output DEF file [required]' say ' -name : Library name [default: DEF file w/o extension]' say ' -version : Library version [default: none]' say ' -template : DEF file template [default: none]' say ' -map : Input/output map file [default: none]' /* say ' -q : Be quiet [default: be loud]' */ say say ' is one or more object files to process (wildcards' say 'are NOT supported).' say say ' contains options and input files delimited with' say 'spaces, tabs or new line charcters. Options from response files' say 'are processed first, so command line options override them.' say say 'The default export mode is "export by name". In this mode, every public' say 'symbol is exported using its mangled name and ordinals are not explicitly' say 'assigned. This mode is suitable for exporting a relatively small amount' say 'of symbols.' say say 'Specifying a non-empty -map option turns on the "export by ordinal"' say 'mode. In this mode, public symbols are exported by ordinals only,' say 'which is very efficient when there are thousands of symbols. The given' say 'map file is used to store ordinals assigned to every symbol in order to' say 'make these assignments persistent from build to build.' say say 'If -template option is not empty, the given file is used as a template' say 'to generate the resulting DEF file. The following macros are recognized:' say say '- ${name} is replaced with the library name;' say '- ${version} is replaced with the library version;' say '- ${exports} is replaced with the list of exported entries.' call Done 0 /** * Returns a list of all words from the string as a stem. * Delimiters are spaces, tabs and new line characters. * Words containg spaces must be enclosed with double * quotes. Double quote symbols that need to be a part * of the word, must be doubled. * * @param string the string to tokenize * @param stem * the name of the stem. The stem must be global * (i.e. its name must start with 'G.!'), for example, * 'G.!wordlist'. * @param leave_ws * 1 means whitespace chars are considered as a part of words they follow. * Leading whitespace (if any) is always a part of the first word (if any). * * @version 1.1 */ TokenizeString: procedure expose (Globals) parse arg string, stem, leave_ws leave_ws = (leave_ws == 1) delims = '20090D0A'x quote = '22'x /* " */ num = 0 token = '' len = length(string) last_state = '' /* D - in delim, Q - in quotes, W - in word */ seen_QW = 0 do i = 1 to len c = substr(string, i, 1) /* determine a new state */ if (c == quote) then do if (last_state == 'Q') then do /* detect two double quotes in a row */ if (substr(string, i + 1, 1) == quote) then i = i + 1 else state = 'W' end else state = 'Q' end else if (verify(c, delims) == 0 & last_state \== 'Q') then do state = 'D' end else do if (last_state == 'Q') then state = 'Q' else state = 'W' end /* process state transitions */ if ((last_state == 'Q' | state == 'Q') & state \== last_state) then c = '' else if (state == 'D' & \leave_ws) then c = '' if (last_state == 'D' & state \== 'D' & seen_QW) then do /* flush the token */ num = num + 1 call value stem'.'num, token token = '' end token = token||c last_state = state seen_QW = (seen_QW | state \== 'D') end /* flush the last token if any */ if (token \== '' | seen_QW) then do num = num + 1 call value stem'.'num, token end call value stem'.0', num return /** * Returns the name of the temporary directory. * The returned value doesn't end with a slash. */ GetTempDir: procedure expose (Globals) dir = value('TEMP',,'OS2ENVIRONMENT') if (dir == '') then dir = value('TMP',,'OS2ENVIRONMENT') if (dir == '') then dir = SysBootDrive() return dir /** * Replaces all occurences of a given substring in a string with another * substring. * * @param str the string where to replace * @param s1 the substring which to replace * @param s2 the substring to replace with * @return the processed string * * @version 1.1 */ Replace: procedure expose (Globals) parse arg str, s1, s2 l1 = length(s1) l2 = length(s2) i = 1 do while (i > 0) i = pos(s1, str, i) if (i > 0) then do str = delstr(str, i, l1) str = insert(s2, str, i-1) i = i + l2 end end return str /** * Replaces argument markers %1, %2 (up to %Z) in the given string * with the values of the remaining arguments. * * @param string string containing argument markers to replace * @param ... arguments to replace markers with values * * @version 1.1 */ ReplaceArgs: procedure expose (Globals) parse arg str do i = 2 to arg() c = i - 1 if (c >= 10) then c = d2c(65 + (c - 10)) str = Replace(str, '%'c, arg(i)) end return str /** * Prints the error or warning message corresponding to the first * argument (error code). If it starts with 'E_' an error message is * assumed, if it starts with 'W_' -- it's a warning message. * * The special error code 'E_OK' prints 'all done' and exits the * script. * * @param code error or warning code (as a string) * @param ... * arbitrary amount of arguments with additional error information. * These arguments are used to substitute %1, %2 (upto %9) markers * in the error message text. */ Error: procedure expose (Globals) eol = '0D0A'x e.E_CantFindEMXEXP = 'Cannot find EMXEXP.EXE in the PATH.' e.E_CantOpenFile = 'Cannot open file ''%1'' (RC = %2).' e.E_CantDeleteFile = 'Cannot delete file ''%1'' (RC = %2).' e.E_InvalidOption = 'Invalid option: ''%1''.' e.E_FileNotFound = 'File not found: ''%1''.' e.E_NoDefFile = 'No output DEF file.' e.E_NoObjectFiles = 'No input object files.' e.E_EMXEXPFailed = 'EMXEXP.EXE failed with RC = %1.' e.E_NoExports =, 'The specified list of objects doesn''t export any symbols.' e.E_InvalidMagicSeg =, 'Invalid or missing magic segment ''%2'' for symbol "%1" in ''%3'' (line %4).' e.E_InvalidOrdinal =, 'Invalid or missing ordinal ''%2'' for symbol "%1" in ''%3'' (line %4).' e.E_NoNoname =, 'No NONAME keyword for symbol "%1" in ''%2'' (line %3).' e.E_DuplicateSymbol =, 'Duplicate symbol symbol "%1" detected in ''%2'' (line %3).' parse upper arg code str = value('e.'code) do i = 2 to arg() str = Replace(str, '%'||(i-1), arg(i)) end if (left(code,2) == 'E_') then str = 'ERROR: 'str else if (left(code,2) == 'W_') then str = 'WARNING: 'str else str = 'Something is really wrong...' if (word(SysCurPos(), 2) > 0) then say say str if (left(code,2) == 'E_') then call done 1 return code /** * NoValue signal handler. */ NoValue: errl = sigl say say say 'EXPRESSION HAS NO VALUE at line #'errl'!' say say 'This is usually a result of a misnamed variable.' say 'Please contact the author.' call Done 252 /** * Nonsense handler. */ Nonsense: errl = sigl say say say 'NONSENSE at line #'errl'!' say say 'The author decided that this point of code should' say 'have never been reached, but it accidentally had.' say 'Please contact the author.' call Done 253 /** * Syntax signal handler. */ Syntax: errn = rc errl = sigl say say say 'UNEXPECTED PROGRAM TERMINATION!' say say 'REX'right(errn , 4, '0')': 'ErrorText(rc)' at line #'errl say say 'Possible reasons:' say say ' 1. Some of REXX libraries are not found but required.' say ' 2. You have changed this script and made a syntax error.' say ' 3. Author made a mistake.' say ' 4. Something else...' call Done 254 /** * Halt signal handler. */ Halt: say say 'CTRL-BREAK pressed, exiting.' call Done 255 /** * Always called at the end. Should not be called directly. * * @param code exit code */ Done: procedure expose (Globals) parse arg code /* protect against recursive calls */ if (value('G.!Done_done') == 1) then exit code call value 'G.!Done_done', 1 /* cleanup stuff goes there */ if (symbol('G.!ObjectListFile') == 'VAR') then if (G.!ObjectListFile \== '') then call SysFileDelete G.!ObjectListFile /* finally, exit */ exit code