text:
/*=====================================================================
Program Name            : fmtlist.sas
Purpose                 : Prints contents of one or more format or
                          informat entries.

SAS Version             : SAS 9.1.3
Input Data              : None
Output Data             : None

Macros Called           : parmv, seplist

Originally Written by   : Scott Bass
Date                    : 04JUL2005
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 18SEP2013
Change/reason           : Complete rewrite of macro based on features
                          available in SAS 9.3
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

proc format lib=work.formats;
  value $chrfmt       "A"="A";
  value numfmt         2 ="B";
  invalue $chrinfmt   "C"="C";
  invalue numinfmt    "D"=4;
run;

%fmtlist()
   Lists all formats in the fmtsearch path.

%fmtlist(doesnotexist)
   Prints a message to the log since no formats match the filter.

%fmtlist(.*fmt doesnotexist)
   Lists all formats in the fmtsearch path
   with name ending in "fmt".
   Note that non-existent format names are silently ignored.

%fmtlist(age.* .*desc .*current.* assid branch)
   Lists all formats in the fmtsearch path
   with name beginning with "age",
   or ending with "desc",
   or containing the word "current",
   or exactly named "assid" or "branch"

%fmtlist(catalog=doesnot.exist)
   Prints a message to the log since no formats match the filter.

%fmtlist(catalog=apfmtlib.formats work.formats doesnot.exist)
   Lists all formats in the apfmtlib.formats or work.formats catalogs,
   if those catalogs are in the fmtsearch path.
   Note that non-existent format catalogs are silently ignored.

%fmtlist(catalog=work.formats)
   Lists all formats in the work.formats catalog,
   if that catalog is in the fmtsearch path.

%fmtlist(catalog=work.formats, type=CF NF)
  Lists all >>>character formats<<< and >>>numeric formats<<<
  from the work.formats catalog.

%fmtlist(type=NF NI)
  Lists all >>>numeric formats<<< and >>>numeric informats<<<
  in the fmtsearch path.

%fmtlist(catalog=work.formats,details=Y)
  Prints the DETAILS of all formats in the work.formats catalog,
  rather than just listing their names,
  if that catalog is in the fmtsearch path.

%fmtlist(age.*des chr.*,catalog=meta.formats work.formats,details=Y)
  Prints the DETAILS of formats with names beginning with "age*des" or "chr",
  in the meta.formats and work.formats catalogs,
  if those catalogs are in the fmtsearch path.

-----------------------------------------------------------------------
Notes:

This macro uses the metadata from the dictionary.formats view.
This dictionary view only lists (built-in and) user-defined formats that
are in the current fmtsearch path.  Therefore, only user-defined formats
in the current fmtsearch path are listed or printed.  This is acceptable,
since these are the only formats available.

By default, this macro lists format names in the output destination.
Specify DETAILS=Y to print the format details in the output destination.

The format list uses Perl Regular Expression (PRX) format to filter
the output (vs. say SQL LIKE syntax).  Each token is bound by the
word break metacharacter (\b) to prevent "false hits" on the filter.

In particular, the wildcard for "all characters" is "dot-asterisk" (.*),
not just asterisk.

See this link (or Google "Perl Regular Expressions") for more details:
http://support.sas.com/documentation/cdl/en/lefunctionsref/64814/HTML/default/viewer.htm#p0s9ilagexmjl8n1u7e1t1jfnzlk.htm

Specify multiple format names and/or wildcards as a space separated list.
The PRX "or" operator (|) will be added by the macro.

Specify the CAT= parameter to limit the search to a single catalog in
the fmtsearch path.  Otherwise, all format catalogs are searched.

Specify the TYPE= parameter to limit the search to one or more format
types:

   CF=Character Format      (Accepts character input, returns character output)
   NF=Numeric Format        (Accepts numeric input, returns character output)
   CI=Character Informat    (Accepts character input, returns character output)
   NI=Numeric Informat      (Accepts character input, returns numeric output)

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

%macro fmtlist
/*---------------------------------------------------------------------
Prints contents of one or more format or informat entries
---------------------------------------------------------------------*/
(LIST          /* List of one or more formats or informats (Opt).    */
               /* If not specified, all formats are printed.         */
               /* Specify wildcards using PRX format, as a           */
               /* space-separated list.                              */
,CATALOG=      /* Format catalog(s) to search (Opt).                 */
               /* If not specified, uses the current value of the    */
               /* FMTSEARCH option to locate the format entry.       */
               /* If specified, use a space-separated list of        */
               /* catalogs in the fmtsearch path to limit the search */
               /* to only those catalogs.                            */
,TYPE=         /* Filter output to specified format type(s) (Opt).   */
               /* If not specified, all format types are listed.     */
               /* Valid values are CF, NF, CI, and NF.               */
,DETAILS=N     /* Print format details instead of just listing the   */
               /* format name? (Req.)                                */
               /* If N, only the format names are printed.           */
               /* If Y, the format details are printed.              */
               /* OFF N NO F FALSE and ON Y YES T TRUE               */
               /* (case insensitive) are acceptable aliases for      */
               /* 0 and 1 respectively.                              */
);

%local macro parmerr where temp flag;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(LIST,         _req=0,_words=1,_CASE=U)
%parmv(CATALOG,      _req=0,_words=1,_CASE=U)
%parmv(TYPE,         _req=0,_words=1,_CASE=U,_VAL=CF NF CI NI)
%parmv(DETAILS,      _req=1,_words=0,_CASE=U,_VAL=0 1)

%if (&parmerr) %then %goto quit;

%* build where clause ;
%let where=;
%if (%superq(list) ne ) %then %do;
  %let temp=%seplist(%superq(list),dlm=|,prefix=\b,suffix=\b);
  %let temp=%unquote(&temp);
  %let where=prxmatch("/&temp/io",objname);
%end;
%if (&catalog ne ) %then %do;
  %let temp=%seplist(&catalog,nest=qq);
  %let temp=%unquote(&temp);
  %if (%superq(where) ne ) %then %let where=&where and;
  %let where=&where catx(".",libname,memname) in (&temp);
%end;
%if (&type ne) %then %do;
  %let temp=%seplist(&type,nest=qq);
  %let temp=%unquote(&temp);
  %if (%superq(where) ne ) %then %let where=&where and;
  %let where=&where fmttype in (&temp);
%end;
%let where=%unquote(&where);
%let flag=%eval(%superq(where) ne );

%* Create view of all the user-defined formats in the current fmtsearch path ;
%if (not %sysfunc(exist(work.usrfmts,view))) %then %do;
proc sql;
  create view work.usrfmts as
    select
      libname,
      memname,
      objname,
      fmtname,
      cats(ifc(substr(fmtname,1,1)="$","C","N"),fmttype) as fmttype length=2
    from
      dictionary.formats
    where
      libname is not missing
    order by
      libname, memname
  ;
quit;
%end;

%* If just listing format names, output to print location ;
%if (not &details) %then %do;
  proc sql print;
    select
      *
    from
      work.usrfmts
    %if (&flag) %then %do;
    where
      &where
    %end;
    order by
      objname
    ;
  quit;
  %if (&sqlobs eq 0) %then %do;
    %put NOTE: No formats matching the filter were found in the fmtsearch path.;
    %goto quit;
  %end;
%end;

%* Ok, we want the format details, not just the format names ;
%* We need to process each format catalog in the fmtsearch path individually ;
%else %do;
  %* get a list of the format catalogs ;
  proc sql noprint;
    select distinct
      catx(".",libname,memname) into :fmtcats separated by " "
    from
      work.usrfmts
    %if (&flag) %then %do;
    where
      &where
    %end;
    ;
  quit;
  %if (&sqlobs eq 0) %then %do;
    %put NOTE: No format catalogs matching the filter were found in the fmtsearch path.;
    %goto quit;
  %end;

  %* Now loop over each format catalog ;
  %macro code;
    %let formats=;
    %if (&flag) %then %do;
      proc sql noprint;
        select
          cats(ifc(fmttype in ("CI","NI"),"@",""),fmtname) into :formats separated by " "
        from
          work.usrfmts
        where
          catx(".",libname,memname)="&word"
        %if (&flag) %then %do;
          and
          &where
        %end;
        order by
          objname
        ;
      quit;
      %if (&sqlobs eq 0) %then %do;
        %put NOTE: No formats matching the filter were found in the fmtsearch path.;
      %end;
    %end;
    %if (not &flag or &sqlobs ne 0) %then %do;
      %* Now call PROC FORMAT to print the format details ;
      title "&word";
      proc format lib=&word fmtlib page;
        %if (&formats ne ) %then %do;
        select &formats;
        %end;
      run;
      title;
    %end;
  %mend;
  %loop(&fmtcats)
%end;

%quit:
%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : attrib.sas
Purpose                 : Generate attrib statements from a template
                          dataset
SAS Version             : SAS 9.4
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 31MAR2016
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

data _null_;
   %attrib(sashelp.class)

   %attrib(sashelp.cars,keep=make model enginesize horsepower doesnotexist)

   %attrib(sashelp.shoes,drop=product stores returns doesnotexist)
run;

Displays attrib statements for sashelp.class, sashelp.cars, and
sashelp.shoes, honoring the keep and drop parameters.

Note that a non-existant variable is silently ignored on a drop or
keep parameter.

=======================================================================

* dummy long formats ;
proc format;
   value $ffffffffffgggggggggghhhhhhhhhhx
      other=[$200.]
   ;
   invalue $iiiiiiiiiijjjjjjjjjjkkkkkkkkkk
      other=_same_
   ;
run;

data template / view=template;
   set sashelp.shoes;
   attrib
      longvar
      length=$32767
      format=$ffffffffffgggggggggghhhhhhhhhhx9.
      informat=$iiiiiiiiiijjjjjjjjjjkkkkkkkkkk9.
      label="This is the label"
   ;
   keep _character_;
   stop;
run;

data _null_;
   %attrib(template);
run;

Variable lists are not supported by this macro.  If you wish to use
variable lists, first create a template view of the desired dataset.

=======================================================================

%attrib(sashelp.shoes,show=Y)
%attrib(sashelp.cars,show=Y,keep=make model enginesize horsepower doesnotexist)
%attrib(sashelp.shoes,show=Y,drop=product stores returns doesnotexist)
%attrib(template,show=Y)

Echos the structure of the dataset, i.e. the generated attrib statements,
to the log, without actually generating attrib statements.

Similar to the data _null_ step above, but does not require MPRINT to
be turned on.

-----------------------------------------------------------------------
Notes:

In a "old style" data step, the following constructs are often used to
achieve similar end results to this macro:

data whatever;
   if 0 then set template_dataset (drop= or keep=);
   ...
run;

or

data whatever;
   set template_dataset (obs=0) source_dataset;
   ...
run;

However, there are (at least) two issues:

1) There is an implicit retain of all data set variables.  If the
template dataset is defining the attributes of a derived variable,
this can be problematic and sometimes results in hard-to-track down
bugs.

2) In PROC DS2, the data source can be either a data set or a FedSQL
query.  Often the template data set requires the (locktable=share)
data set option or other problems can occur.

The different syntax of PROC DS2 can make the use of a template
dataset problematic.

This macro can also be handy if you just want to quickly display
the structure of a dataset to the log:

%attrib(sashelp.zipcode,show=Y)

If show=Y the log will display better with options ls=max;
so that the long log lines do not wrap.

It's also possible to cut-and-paste these lines to another program
to easily duplicate the structure of the template dataset.

I have chosen to design this macro so that the SHOW functionality
inserts whitespace in order to line up the
length/format/informat/label data.

I believe this makes the data easier to skim in the log, but it may not
be your preferred approach, say when cut-and-pasting this into another
program.

A potential workaround is to cut-and-paste the MPRINT output.  The
macro tokenizer will parse out the extra whitespace, and echo these
results to the SAS log.  These results are the actual code that the SAS
compiler receives from the macro tokenizer.

Press Alt-LMB to block copy MPRINT text within EG and the SAS editor.

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

%macro attrib
/*---------------------------------------------------------------------
Generate attrib statements from a template dataset.
---------------------------------------------------------------------*/
(DATA          /* Template dataset or view (REQ).                    */
               /* Data set options are not supported.                */
,KEEP=         /* Space separated list of variables to keep (Opt.)   */
               /* Case-insensitive.  Non-existent variables are      */
               /* silently ignored.  Variable lists not supported.   */
,DROP=         /* Space separated list of variables to drop (Opt.)   */
               /* Case-insensitive.  Non-existent variables are      */
               /* silently ignored.  Variable lists not supported.   */
,SHOW=N        /* Display the dataset attributes without actually    */
               /* generating executable attrib statements? (Opt.)    */
               /* Default value is NO.  Valid values are:            */
               /* 0 1 OFF N NO F FALSE and ON Y YES T TRUE           */
               /* OFF N NO F FALSE and ON Y YES T TRUE               */
               /* (case insensitive) are acceptable aliases for      */
               /* 0 and 1 respectively.                              */
);

%local macro parmerr;
%local attrib name type length format informat label varlist;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(DATA,         _req=1,_words=0,_case=N)
%parmv(KEEP,         _req=0,_words=1,_case=U)
%parmv(DROP,         _req=0,_words=1,_case=U)
%parmv(SHOW,         _req=0,_words=0,_case=U,_val=0 1)

%if (&parmerr) %then %goto quit;

%* additional error checking ;

%* parse off any dataset options ;
%let pos=%index(&data,%str(%());
%if (&pos) %then %let data=%substr(&data,1,&pos-1);

%* does the dataset or view exist? ;
%if not (%sysfunc(exist(&data,data)) or %sysfunc(exist(&data,view))) %then %do;
   %parmv(_msg=&data does not exist.);
   %goto quit;
%end;

%* initialize varlist ;
%let varlist=;

%let dsid=%sysfunc(open(&data,i));
%if (&dsid le 0) %then %do;
   %parmv(_msg=Unable to open &data);
   %goto quit;
%end;
%let numvars=%sysfunc(attrn(&dsid,nvars));
%do i=1 %to &numvars;
   %let name      =%sysfunc(varname(&dsid,&i));
   %let type      =%sysfunc(vartype(&dsid,&i));
   %let length    =%sysfunc(varlen(&dsid,&i));
   %let format    =%sysfunc(varfmt(&dsid,&i));
   %let informat  =%sysfunc(varinfmt(&dsid,&i));
   %let label     =%sysfunc(varlabel(&dsid,&i));

   %* use a marker token (~) to preserve spacing by the macro tokenizer ;
   %let name=~%sysfunc(putc(&name,$32.))~;

   %let length=length=$%sysfunc(putn(&length,6.-R));
   %if (&type eq N) %then
   %let length=%sysfunc(translate(&length,%str( ),%str($)));
   %let length=~&length~;

   %if (&format ne ) %then
   %let format=~format=%sysfunc(putc(&format,$35.))~;
   %else
   %let format=~       %sysfunc(repeat(%str( ),34))~;

   %if (&informat ne ) %then
   %let informat=~informat=%sysfunc(putc(&informat,$35.))~;
   %else
   %let informat=~         %sysfunc(repeat(%str( ),34))~;

   %if (%superq(label) ne ) %then
   %let label=label="&label";

   %* use a diffent marker token for the label, ;
   %* since a valid label can contain ~ ;
   %let attrib=attrib;
   %let attrib=&attrib&name&length&format&informat#####;
   %let attrib=%sysfunc(translate(&attrib,%str( ),%str(~)));
   %let attrib=%sysfunc(strip(&attrib))&label;
   %let attrib=%sysfunc(transtrn(&attrib,%str(#####),));

   %* remove ~ from name for varlist processing ;
   %let name=%sysfunc(translate(&name,%str( ),%str(~)));
   %let name=%left(&name);

   %if (&keep ne ) %then %do;
      %if (%index(&keep,%upcase(&name))) %then %do;
         %if (&show) %then %do;
            %put %str(&attrib;);
         %end;
         %else %do;
            &attrib;
            %let varlist=&varlist &name;
         %end;
      %end;
   %end;
   %else
   %if (&drop ne ) %then %do;
      %if ^(%index(&drop,%upcase(&name))) %then %do;
         %if (&show) %then %do;
            %put %str(&attrib;);
         %end;
         %else %do;
            &attrib;
            %let varlist=&varlist &name;
         %end;
      %end;
   %end;
   %else %do;
      %if (&show) %then %do;
         %put %str(&attrib;);
      %end;
      %else %do;
         &attrib;
         %let varlist=&varlist &name;
      %end;
   %end;
%end;
%if ^(&show) %then %do;
call missing(of &varlist);  %* prevents uninitialized variable messages ;
%end;

%quit:
%let dsid=%sysfunc(close(&dsid));

%mend;

/******* END OF FILE *******/

repo_name: scottbass/SAS

repo_language: SAS

mime_type: text/plain


text:
/******************************************************************
*
* The %LOGPARSE() macro extracts performance statistics from a
* SAS log file.
*
* See the README file for instructions and syntax.
*
*******************************************************************/

%macro logparse(saslog, outds, system, pdsloc, append=NO );

   /***************************************************************
   * If no system was specified, set it to the ID of the system
   * under which this proram is running, on the assumption that
   * one kind of computer is being used for both generating and
   * analyzing the logs.
   ****************************************************************/
   %if ( &system = )  %then  %let system = &sysscp;

   /***************************************************************
   * Normalize the value of the APPEND= keyword parameter.
   ****************************************************************/
   %let append = %upcase( &append );
   %if ( &append = Y  ) %then %do;  %let append = YES;  %end;
   %if ( &append = YE ) %then %do;  %let append = YES;  %end;
   %if ( &append = N  ) %then %do;  %let append = NO;   %end;

   /***************************************************************
   * Make sure APPEND= is now YES or NO.
   ****************************************************************/
   %if ( &append ne YES ) and ( &append ne NO )  %then %do;
      %put ERROR: The value of the APPEND= parameter must be YES or NO.;
      %goto exit;
   %end;

   /***************************************************************
   * If they asked for this invocation to append its output, make
   * sure a file name was supplied.
   ****************************************************************/
   %if ( &append = YES )  %then %do;
      %if ( &outds = )  %then %do;
         %put ERROR: When append=yes is specified, an output file name;
         %put        (second parameter) must also be specified.;
         %goto exit;
      %end;
   %end;

   /***************************************************************
   * Create requested data set using performance statistics found
   * in the LOG.
   ****************************************************************/
   %if ( &append = YES )  %then %do;
      data _sas_logparse_temp_1_;
   %end;
   %else %do;
      data &outds;
   %end;

      length logfile $ 200 stepname $ 20
             line $200 upcase_Line $ 200 prevline $ 200
             portdate $ 25 keyword $ 20
             platform scp $ 50 /* UNIX needs this length */
             prevblanks blanks coloncnt reprocess
             realtime usertime systime cputime
             pageflt pagercl pageswp osvconsw osiconsw
             excsemap shrsemap consemap
             blkinput bkoutput buffio dirio
             vatime vutime rsmtime excpcnt
             tskmemd tskmemp totmemd totmemp belowmem abovemem
             obsin obsout varsout
             threads events locks pools poolsx 8;

      format realtime time12.3 usertime time12.3
             systime time12.3
             datetime datetime.
             cputime time12.3;

      label  bkoutput = 'Block Output Operations'
             blkinput = 'Block Input Operations'
             buffio = 'Buffered IO'
             consemap = 'Contended Semaphores'
             cputime = 'CPU Time'
             datetime = 'Run Date/Time'
             dirio = 'Direct IO'
             events = 'Events'
             excpcnt = 'I/O count'
             excsemap = 'Exclusive Semaphores'
             host = 'System Name'
             locks = 'Locks'
             logfile = 'Log file name'
             memused = 'Memory Used'
             osmem = 'OS Memory Used'
             obsin = 'Observations Read'
             obsout = 'Observations Written'
             osiconsw = 'Involuntary Context Switches'
             osvconsw = 'Voluntary Context Switches'
             pageflt = 'Page Faults'
             pagercl = 'Page Reclaims'
             pageswp = 'Page Swaps'
             platform = 'Platform'
             pools = 'Memory Pools Created'
             poolsx = 'Memory Pools Destroyed'
             portdate = 'SAS Port Date'
             realtime = 'Elapsed Time'
             rsmtime = 'RSM Hiperspace'
             scp = 'Operating System'
             shrsemap = 'Shared Semaphores'
             stepcnt = 'Step Number'
             stepname = 'Step Name'
             systime = 'System Time'
             threads = 'Threads'
             totmemd = 'Total Memory - data'
             totmemp = 'Total Memory - program'
             tskmemd = 'Task Memory - data'
             tskmemp = 'Task Memory - program'
             belowmem = 'Total Memory - below line'
             abovemem = 'Total Memory - above line'
             usertime = 'User Time'
             varsout = 'Variables Written'
             vatime = 'Vector Affinity'
             vutime = 'Vector Usage'
             ;

      retain line upcase_Line prevline blanks prevblanks logfile
             obsin obsout varsout;
      retain stepcnt 0
             hdrfnd 0         /* 0=no hdr yet, 1=hdr fnd, 2=hdr finish*/
             portdate
             scp
             datetime
             platform         /* defined in RELNAME routine           */
             host
             ;

      /*****************************************************************
      * Moved the DROP list down here instead of an option on the DATA
      * statement to avoid generating a syntax error if no output data
      * set name is specified on the macro invocation.
      *****************************************************************/
      drop   line prevline reprocess coloncnt inbuf pos upcase_Line
             prevblanks blanks indxFld indxLen hdrfnd keyword _I_ len;

      %let ar_bnd = 3;        /* change value when array is changed   */
      array ar_hdr (&ar_bnd) $20  _temporary_ ('PASS HEADER'
                                               'APR HEADER'
                                               'WTEST HEADER');

      %if ("&sysscp" = "OS") %then %do;
         %if (%sysfunc(fileexist(".&saslog..logs"))) %then %do;
           infile ".&saslog..logs" end=lastrec pad truncover;
         %end;
         %else %do;
           infile ".&pdsloc..logs(&saslog)" end=lastrec pad truncover;
         %end;
      %end;

      %else %do;
         infile "&saslog" lrecl=250 end=lastrec pad truncover;
      %end;

      logfile="&saslog";

      /************************************************************
      * GETLINE:
      * Read in a line from the LOG file, skipping over blank
      * lines as well as page breaks.  Page breaks can be denoted
      * by "THE SAS SYSTEM" title line, but this string can also
      * appear in a NOTE: statement, so skip the former but keep
      * the latter.  Also skip lines which begin with a number,
      * like a right-justified datetime stamp.  Ordinarily such
      * lines appear on the same line as "THE SAS SYSTEM", but if
      * the user has set the LINESIZE option to a small number,
      * they are sometimes placed on the next line.
      * Stop if we hit the end of the file.
      * Remove any carriage returns (presumably from PC runs).
      * Save off the previous and current amount of indentation
      * to detect whether we're still in a FULLSTIMER block.
      *************************************************************/
      %macro getline;
         if (lastrec = 0) then do;
            prevline = line;
            do until ( (lastrec = 1) or
                       (upcase_Line =: 'NOTE:') or
                       ((compress(line) ne '') and
                        (substr(line,1,1) ne '0c'x) and
                        (compress(scan(upcase_line,1,' '),
                                  ':0123456789') ne '') and
                        (index(upcase_Line, 'THE SAS SYSTEM') le 0)) );
               input line $char200.;
               upcase_Line = upcase(line);
            end;

            line = compress(line, '0d'x);

            prevblanks = blanks;
            blanks = length(line) - length(left(line));
         end;
         else do;
            stop;
         end;
      %mend getline;

      /************************************************************
      * Read in the very first line of the LOG.
      * All subsequent lines are read in at the bottom of the loop.
      * The algorithm is written in this way to get around a
      * problem by which SAS detects a false looping condition if
      * no INPUT statement is executed in a given DATA STEP
      * iteration.
      *************************************************************/
      if (_n_ = 1) then do;
         %getline;
      end;

      /************************************************************
      * Certain steps are preceded with NOTEs containing
      * the number of observations read and/or the number of
      * observations and variables written.  Accumulate these
      * values to be written with the next observation.
      *************************************************************/
      if (index(upcase_Line,'NOTE: THERE WERE ') and
          index(upcase_Line,' OBSERVATIONS READ FROM THE DATA SET')) then do;
         obsin + input(scan(line,4,' '), 20.);
      end;
      else if (index(upcase_Line, 'NOTE: THE DATA SET ') and
               index(upcase_Line, ' OBSERVATIONS AND ') and
               index(upcase_Line, ' VARIABLES.')) then do;
         obsout + input(scan(line,7,' '), 20.);
         varsout + input(scan(line,10,' '), 20.);
      end;

      /************************************************************
      * Process PASS HEADER information
      * Look for PASS HEADER keywords
      * Each line can contain only one keyword,
      * so quit looking when a keyword is found
      *************************************************************/
      if (line = ' ' and hdrfnd = 1) then
         hdrfnd = 2;                                /* header finished */
      if (hdrfnd = 0 and (portdate = ' ' or
                          platform = ' ' or
                          scp = ' ')) then do;
         link RELNAME;
      end;

      if (hdrfnd = 0 or hdrfnd = 1) then do;       /* check for header info */
         do _I_ = 1 to &ar_bnd;
            keyword = ar_hdr(_I_);
            /* search keyword, trim needed */
            pos = index(upcase_Line, trim(keyword));
            if pos > 0 then do;                    /* keyword found */
               len = length(keyword);
               _I_ = &ar_bnd + 1;                  /* stop looking */
            end;
         end;
         keyword = ' ';                            /* clear last search lit */
         if (pos ge 1 and pos le 5) then do;       /* must be in line begining*/
            hdrfnd = 1;
            upcase_Line = substr(upcase_Line, pos + len);
            keyword = left(scan(upcase_Line,1,'='));/* find var indicator */
            select;                                /* scan out var value */
               when (keyword = 'OS')
                  scp = scan(upcase_Line,2,"=");
               when (keyword = 'HOST')
                  host = scan(upcase_Line,2,"=");
               when (keyword = 'VER')
                  portdate = scan(upcase_Line,2,"=");
               when (keyword = 'DATE') do;
                  datetime = input(scan(upcase_Line,2,"="),datetime.);
               end;
               /* add additional variable value scans here */
               otherwise;
            end;  /* select */
         end;  /* if (pos ge 1 and pos le 5) */
      end;  /* if (hdrfnd = 0 or hdrfnd = 1) */

      /************************************************************
      * Handle MVS as a special case.
      *************************************************************/
      %if ("&system" = "MVS" or "&system" = "OS") %then %do;

         /*********************************************************
         * As soon as we detect the first statistic in the current
         * block, start reading them all in.  This is done by
         * looking for the "CPU" string in the current line.  If
         * we're dealing with MVS JCL job output, there may be
         * additional lines preceding the actual SAS log which
         * contain "CPU".  Such lines are ignored if we can't
         * parse the cputime statistic from the line using known
         * SAS log syntax.
         * If we detect the SESSION-ending CPU line, output and
         * stop.
         **********************************************************/
         if (index(line, 'CPU') gt 0) then do;
            if (index(upcase_Line,
                      "INITIALIZATION PHASE USED ") > 0) then do;
               cputime=input(scan(line,6,' '), 20.) ;
               stepname=scan(line, 3, ' ') ;
               memused=input(scan(line,10,' (K'), 20.) ;

               /***************************************************
               * Check to see if the next line contains the
               * above/below line address space NOTE.  If it
               * doesn't, make sure we reprocess the line.
               ****************************************************/
               %getline;
               %macro memline;
                  if (index(upcase_Line, 'NOTE: THE ADDRESS SPACE HAS ' ||
                                         'USED A MAXIMUM OF') gt 0) then do;
                     belowmem = input(scan(line,10,' K'), 20.);
                     abovemem = input(scan(line,15,' K'), 20.);
                  end;
                  else do;
                     reprocess = 1;
                  end;
               %mend memline;
               %memline;
            end;

            else if (index(upcase_Line, "SAS SESSION USED") > 0) then do;
               cputime=input(scan(line,6,' '), 20.) ;
               stepname=scan(line,3,' ') ;
               memused=input(scan(line,10,' (K'), 20.) ;

               /***************************************************
               * Check to see if the next line contains the
               * above/below line address space NOTE.
               * Output and stop.
               ****************************************************/
               %getline;
               %memline;

               output;
               stop;
            end;

            else if (index(upcase_Line, "CPU     TIME -") > 0) then do;
               if (index(prevline, "DATA ") > 0) then
                  stepname=scan(prevline, 3, ' ');
               else
                  stepname=scan(prevline, 4, ' ');

               inbuf = scan(line, 4, ' ');
               cputime=input(inbuf, time12.3);

               /***************************************************
               * Read in the remaining FULLSTIMER lines.  We keep
               * reading as long as the line is indented the same
               * number of characters (as FULLSTIMER blocks are).
               ****************************************************/
               %getline;
               do while (blanks = prevblanks);

                  /************************************************
                  * Parse individual statistics.
                  *************************************************/
                  if (index(upcase_Line,"ELAPSED TIME -") > 0) then do;
                     inbuf = scan(line, 4, ' ');
                     realtime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,
                                 "VECTOR AFFINITY TIME -") > 0) then do;
                     inbuf = scan(line, 5, ' ');
                     vatime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,
                                 "VECTOR USAGE    TIME -") > 0) then do;
                     inbuf = scan(line, 5, ' ');
                     vutime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,
                                 "RSM HIPERSPACE  TIME -") > 0) then do;
                     inbuf = scan(line, 5, ' ');
                     rsmtime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,"EXCP COUNT") > 0) then do;
                     excpcnt = input(scan(line,4,' '), 20.);
                  end;
                  else if (index(upcase_Line,"TASK  MEMORY -") > 0) then do;
                     memused = input(scan(line,4,' K'), 20.);
                     tskmemd = input(scan(line,5,' (K'), 20.);
                     tskmemp = input(scan(line,7,' K'), 20.);
                  end;
                  else if (index(upcase_Line,"TOTAL MEMORY -") > 0) then do;
                     totmemd = input(scan(line,5,' (K'), 20.);
                     totmemp = input(scan(line,7,' K'), 20.);
                  end;

                  %getline;
               end;  /* do while (blanks = prevblanks) */

               /***************************************************
               * Check to see if the next line contains the
               * above/below line address space NOTE.  If it
               * doesn't, make sure we reprocess the line.
               ****************************************************/
               %memline;

            end;  /* else if (index(upcase_Line, "CPU     TIME -") > 0) */

            /******************************************************
            * If we at least found the CPU statistic...
            *  Output an observation corresponding to the current
            *  block of statistics.
            *  Clear out the retained variables which contain info
            *  from prior NOTE lines.
            *******************************************************/
            if (cputime ne .) then do;
               output;
               stepcnt+1;
               obsin = .;
               obsout = .;
               varsout = .;
            end;

         end;  /* if ( index(line, "CPU") > 0 ) */
      %end;  /* %if ("&system" = "MVS") */

      /************************************************************
      * All other systems share the same basic FULLSTIMER format.
      *************************************************************/
      %else %do;

         /*********************************************************
         * If we detect a line beginning "Total Elapsed Time", we
         * must be parsing a Java Test Client Application that is
         * exercising an OMR server.  If so, simply read this line
         * in as the sole statistic to be captured for this test.
         * We are currently ignoring "Elapsed Time" lines in these
         * files since there were too many of them and they were
         * making the qatrack PERFREL data sets too large.
         **********************************************************/
         if (index(line, "Total Elapsed Time:") = 1) then do;
            realtime = input(scan(line, 4, ' '), 20.);
            stepname = 'Total';
            output;
            stepcnt+1;
         end;

         /*********************************************************
         * As soon as we detect the first statistic in the current
         * block, start reading them all in.
         **********************************************************/
         else if (index(line, "real time ") > 0) then do;
            if (index(prevline, 'DATA ') > 0) then
               stepname=scan(prevline, 2, ' ');
            else
               stepname=scan(prevline, 3, ' ');

            /******************************************************
            * TIMESTAT:
            * Parses statistics that could be in time format
            * (i.e., HH:MM:SS.dd).
            * Use it to parse realtime.
            *******************************************************/
            %macro timestat(var, statword);
               inbuf = scan(line, &statword, ' ');
               coloncnt = length(inbuf) -
                          length(compress(inbuf,':'));

               if (coloncnt = 0) then
                  inbuf = '0:0:' || trim(inbuf);
               else if (coloncnt = 1) then
                  inbuf = '0:' || trim(inbuf);

               &var = input(inbuf, time12.3);
            %mend timestat;

            %timestat(realtime, 3);

            /******************************************************
            * Read in the remaining FULLSTIMER lines.  We stop
            * reading either when a line is found that isn't
            * indented (ignoring page breaks) or we reach the end
            * of the LOG.
            *******************************************************/
            %getline;
            do while (blanks = prevblanks);
               if (index(line,"user cpu time") > 0) then do;
                  %if (("&system" = "ALP") or
                       ("&system" = "VMS_AXP") or
                       ("&system" = "VMS")) %then %do;
                     %timestat(cputime, 4);
                  %end;
                  %else %do;
                     %timestat(usertime, 4);
                  %end;
               end;

               else if (index(line,"system cpu time") > 0) then do;
                  %timestat(systime, 4);
               end;

               else if (index(line,"cpu time") > 0) then do;
                     %timestat(cputime, 3);
               end;

               else if (index(line,"Semaphores") > 0) then do;
                  excsemap=input(scan(line,3," "), 20.);
                  shrsemap=input(scan(line,5," "), 20.);
                  consemap=input(scan(line,7," "), 20.);
               end;

               /***************************************************
               * GETSTAT:
               * Retrieves an integer statistic and places it in
               * the passed variable.
               ****************************************************/
               %macro getstat(text, var);
                  else if (index(line, "&text") gt 0) then do;
                     &var = input(scan(line,-1,': '), 20.);
                  end      /* do NOT put a semi-colon on this line */
               %mend getstat;

               %getstat(Page Faults, pageflt);
               %getstat(Page Reclaims, pagercl);
               %getstat(Page Swaps, pageswp);
               %getstat(Voluntary Context Switches, osvconsw);
               %getstat(Involuntary Context Switches, osiconsw);
               %getstat(Block Input Operations, blkinput);
               %getstat(Block Output Operations, bkoutput);
               %getstat(Buffered IO, buffio);
               %getstat(Direct IO, dirio);
               %getstat(Threads, threads);
               %getstat(Events, events);
               %getstat(Locks, locks);
               %getstat(Memory Pools  Created, pools);
               %getstat(Memory Pools  Destroyed, poolsx);

               else if (index(line,"OS Memory ") > 0) then do;
                  osmem = input(compress(scan(line,3,' '),'k'), 20.);
               end;

               else if (index(line,"Memory ") > 0) then do;
                  memused = input(compress(scan(line,2,' '),'k'), 20.);
               end;

               %getline;
            end;  /* do while (blanks = prevblanks) */

            /******************************************************
            * Make sure we reprocess the last line read in since
            * it is not part of the FULLSTIMER block.
            *******************************************************/
            reprocess = 1;

            /******************************************************
            * Output an observation corresponding to the current
            * block of statistics.
            * Clear out the retained variables which contain info
            * from prior NOTE lines.
            *******************************************************/
            output;
            stepcnt+1;
            obsin = .;
            obsout = .;
            varsout = .;
         end; /* else if ( index(line, "real time ") > 0 ) */
      %end;  /* %else (this is a non-MVS system) */

      /************************************************************
      * If we haven't already, read in the next line.
      *************************************************************/
      if (reprocess ne 1) then do;
         %getline;
      end;

      return;

      /************************************************************
      * Identify the port date and platform.
      *************************************************************/
      RELNAME:
         length indxFld $255;
         indxFld = "PROPRIETARY SOFTWARE PRE-PRODUCTION VERSION";
         pos = indexw(upcase_Line, indxFld);
         if (pos = 0) then do;
            indxFld = "PROPRIETARY SOFTWARE RELEASE";
            pos = indexw(upcase_Line,indxFld);
         end;
         if (pos = 0) then do;
            indxFld = "PROPRIETARY SOFTWARE VERSION";
            pos = indexw(upcase_Line,indxFld);
         end;
         if (pos > 0) then do;
            /* drop initial part of line */
            indxLen = length(trim(indxFld));
            upcase_Line = substr(upcase_Line,pos + indxLen + 1);
            portdate = compress(scan(upcase_Line,-2,' ') || ' ' ||
                                scan(upcase_Line,-1,' '), '()');
         end;
         indxFld = 'THIS SESSION IS EXECUTING ON THE';
         pos = indexw(upcase_Line, indxFld);
         if (pos > 0) then do;
            /* drop initial part of line */
            indxLen = length(trim(indxFld));
            upcase_Line = substr(upcase_Line,pos + indxLen + 1);
            line = substr(line,pos + indxLen + 1);
            /* find end of platform name */
            pos = index(upcase_Line, "PLATFORM");
            if pos > 1 then do;
               /* save original case of platform name */
               platform = substr(line, 1, pos - 1);
            end;
         end;
         indxFld = 'RUNNING ON';
         pos = indexw(upcase_Line, indxFld);
         if (pos > 0) then do;
            /* drop initial part of line */
            indxLen = length(trim(indxFld));
            upcase_Line = substr(upcase_Line,pos + indxLen + 1);
            line = substr(line,pos + indxLen + 1);
            /* find end of platform name */
            pos = index(upcase_Line, ".   ");
            if (pos le 1) then
               pos = length(trim(upcase_Line)) + 1;
            /* save original case of operating system name */
            scp = substr(line, 1, pos - 1);
         end;
      return;  /* RELNAME: */
   run;

   /***************************************************************
   * If they asked for this invocation to append its output:
   * ==> Append the file produced above to the specified file.
   * ==> Delete the temporary file
   ****************************************************************/
   %if ( &append = YES )  %then %do;
      proc datasets  lib=work  nolist;
         append  base=&outds  data=_sas_logparse_temp_1_;
         delete _sas_logparse_temp_1_;
      quit;
   %end;

%exit:  %mend logparse;


text:
/*=====================================================================
Program Name            : get_lib_attr.sas
Purpose                 : Function style macro to return a library
                          attribute.
SAS Version             : SAS 9.2
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 20OCT2011
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

* allocate some sample libraries ;
libname v6    v6      "C:\";
libname v7    v7      "C:\";
libname v9    v9      "C:\";
libname spde  spde    "C:\";
libname excel excel   "C:\Temp\temp.xls";  * this file must exist ;

%put %get_lib_attr(v6,engine);
%put %get_lib_attr(v7,engine);
%put %get_lib_attr(v9,engine);
%put %get_lib_attr(work,engine);
%put %get_lib_attr(sashelp,engine);
%put %get_lib_attr(spde,engine);
%put %get_lib_attr(excel,engine);

* various other attributes ;
%put %get_lib_attr(v6,libname);   * a bit redundant since we supplied a libname! ;
%put %get_lib_attr(sashelp,path); * note it only reports the first level ;
%put %get_lib_attr(spde,fileformat);
%put %get_lib_attr(excel,readonly);
%put %get_lib_attr(spde,sequential);
%put %get_lib_attr(excel,sysdesc);
%put %get_lib_attr(v7,sysname);
%put %get_lib_attr(v9,sysvalue);
%put %get_lib_attr(sashelp,temp);
%put %get_lib_attr(work,temp);

* examples of returning all values of a multi-level library allocation ;
%put %get_lib_attr(sashelp,path,levels=0);

%macro parse_output;
  %put %get_lib_attr(spde,sysname,levels=0);
  %put %get_lib_attr(spde,sysvalue,levels=0);
  %let _sysname=%get_lib_attr(spde,sysname,levels=0);
  %let _sysvalue=%get_lib_attr(spde,sysvalue,levels=0);
  %put sysname=&_sysname;
  %put sysvalue=&_sysvalue;
  %do i=1 %to 4;
    %put %scan(&_sysname,&i,^)=%scan(&_sysvalue,&i,^);
  %end;
%mend;
%parse_output;

* and some error checking ;
%put >>> %get_lib_attr(doesnotexist,engine);  * no error thrown, just returns blank ;

-----------------------------------------------------------------------
Notes:

This macro would mainly be used to return the engine type of an existing
library allocation, but I have allowed all columns present in
sashelp.vlibnam to be passed in as parameters.

Some of these parameters are redundant, eg. path (better retrieved via
the pathname function).

By default, this macro only returns the first record fetched for the
specified libname.  However, if LEVELS=0 is specified, all items in a
multi-level library allocation are returned in a delimited list. This
list would usually be further processed by the calling program.

For a SPDE library, the first sysvalue parameter returns the free space
remaining on the drive.  An esoteric but perhaps handy little feature of
the SPDE engine.

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

%macro get_lib_attr
/*---------------------------------------------------------------------
Function style macro to return a library attribute.
---------------------------------------------------------------------*/
(LIBNAME       /* Input libname (REQ).  Must already be allocated.   */
,ATTR          /* Desired library attribute (REQ).                   */
               /* See code for valid attributes.                     */
,LEVELS=1      /* Desired levels of a multi-level library allocation */
               /* (REQ).                                             */
               /* Usual value would be 1, and is the default value.  */
               /* Specify LEVELS=0 to return all levels of the       */
               /* specified attributes in a delimited list.          */
,DLM=^         /* Delimiter for multi-level library output (REQ).    */
);

%local macro parmerr &attr;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(LIBNAME,      _req=1,_words=0,_case=U)
%parmv(LEVELS,       _req=1,_words=0,_case=U,_val=NONNEGATIVE)
%parmv(DLM,          _req=1,_words=0,_case=U)
%parmv(ATTR,         _req=1,_words=0,_case=U,_val=

/* Library attributes present in sashelp.vlibnam */
LIBNAME
ENGINE
PATH
LEVEL
FILEFORMAT
READONLY
SEQUENTIAL
SYSDESC
SYSNAME
SYSVALUE
TEMP
)

%if (&parmerr) %then %goto quit;

%* open sashelp.vlibnam ;
%let dsid=%sysfunc(open(sashelp.vlibnam (where=(libname="%upcase(&libname)"))));

%* was the open successful? ;
%if (&dsid le 0) %then %do;
   %parmv(_msg=Unable to open sashelp.vlibnam)
   %goto quit;
%end;

%* automatically set macro variables when a record is fetched ;
%syscall set(dsid);

%* if levels=0 then return all items in a delimited list ;
%* else just return the specified record (usually 1) ;
%if (&levels eq 0) %then %do;
  %do %while (%sysfunc(fetch(&dsid)) eq 0);
  %* do not indent the below line ;
%trim(%left(&&&attr)) &dlm
  %end;
%end;
%else %do;
  %let rc=%sysfunc(fetchobs(&dsid,&levels));
  %* do not indent the below line ;
%trim(%left(&&&attr))
%end;
%let dsid=%sysfunc(close(&dsid));

%quit:
%* if (&parmerr) %then %abort;

%mend;

/******* END OF FILE *******/


text:
%** V0000 *****************************************************************************************;
%** Program   : stp_session.sas                                                                  **;
%** On LAN    : No                                                                               **;
%** On Server : Yes                                                                              **;
%** Date      : 13NOV2009                                                                        **;
%** Author    : Michael Dixon                                                                    **;
%** Owner     : BCA                                                                              **;
%** Purpose   : Wrap the STP session functions so that code using them can be used outside       **;
%**             the STP Server                                                                   **;
%**                                                                                **;
%** Notes     : Examples: stp_session(create)      <-- Create a session                          **;
%**                       stp_session(create,600)  <-- Create a session with 10min timeout       **;
%**                       stp_session(delete)      <-- Delete a session                          **;
%**                                                                                **;
%** ---------------------------------------------------------------------------------------------**;
%** History                                                                                **;
%**                                                                                **;
%** Version   :                                                                                **;
%** Date      :                                                                                **;
%** Author    :                                                                                **;
%** Reason    :                                                                                **;
%**                                                                                **;
%**                                                                                **;
%**************************************************************************************************;
%macro stp_session(action,parm1,parm2);
  %let ACTION = %upcase(&ACTION);
  %if %str(&ACTION) = %str( ) %then %let ACTION = CREATE;

  %if (%str(&SYSPROCESSNAME) = %str(Object Server) OR %str(&SYSPROCESSNAME) = %str(OBJECT_EXECUTIVE)) AND &SYSUSERID = sassrv %then %do;
    %* We are running in the STP Server;
    %if &ACTION = CREATE %then %do;
      %if NOT %symexist(_SESSIONID) %then %do;
        %if %str(&PARM1) NE %str( ) %then %let PARM1=%str(,&PARM1);
        %let rc=%sysfunc(stpsrv_session(create &PARM1));
        %put NOTE: STP Session created: &_SESSIONID;
      %end;
      %else %do;
        %put NOTE: STP Session is &_SESSIONID;
      %end;
    %end;
    %else %if &ACTION = DELETE %then %do;
      %if NOT %symexist(_SESSIONID) %then %do;
        %put NOTE: No Session exists - no action taken.;
        %return;
      %end;
      %else %do;
        %let rc=%sysfunc(stpsrv_session(delete));
      %end;
    %end;
  %end;
  %else %do;
    %put NOTE: Not running in STP Server - No action taken.;
  %end;
%mend;


text:
/*=====================================================================
Program Name            : loop_control.sas
Purpose                 : A "wrapper" macro to execute code over a
                          list of items defined in a control table
SAS Version             : SAS 9.2
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 16MAY2016
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 16OCT2019
Change/reason           : Added __iter__ row counter.
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

%macro code;
    %put &name &sex &age &height &weight;
%mend;
%loop_control(control=sashelp.class)

=======================================================================

* The macro variables are not automatically trimmed ;
* Sometimes this is useful, sometimes not ;
%macro cars;
    %put |#&make# *&type* +&model+|;
%mend;
%loop_control(control=sashelp.cars,mname=cars)

=======================================================================

* If you need trimmed macro variables, do so in the child macro ;
%macro cars;
    %let make=%trim(&make);
    %let model=%trim(&model);
    %let type=%trim(&type);
    %put |#&make# *&type* +&model+|;
%mend;
%loop_control(control=sashelp.cars,mname=cars)

=======================================================================

* firstobs and obs dataset options are ignored ;
* see http://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000148395.htm ;
%macro code;
    %put &name &sex &age &height &weight;
%mend;
%loop_control(control=sashelp.class(firstobs=5 obs=10))

=======================================================================

* So, use a view instead ;
data v_class / view=v_class;
   set sashelp.class (firstobs=5 obs=10);
run;
%macro code;
    %put &name &sex &age &height &weight;
%mend;
%loop_control(control=v_class)

=======================================================================

* But a where clause and other dataset options are honoured ;
* This will generate "unresolved macro variable reference" for weight ;
%macro code;
    %put &name &sex &age &height &weight;
%mend;
%loop_control(control=sashelp.class (where=(sex="F") drop=weight))

=======================================================================

* A more complex (albeit contrived) example ;
data metadata;
   length proc $32 data vars $100;
   infile datalines dsd dlm="|";
   input proc data vars;
   datalines;
PRINT    | SASHELP.SHOES (where=(region="Pacific"))  | _all_
SUMMARY  | SASHELP.CLASS   | height weight
FREQ     | SASHELP.CARS    | type
PRINT    | SASHELP.STOCKS (where=(close between 90 and 100))   | stock date close
;
run;

%macro code;
   %let proc=%upcase(&proc);
   %if (&proc eq SUMMARY) %then %do;
      PROC SUMMARY DATA=&data nway
         FW=12
         PRINTALLTYPES
         CHARTYPE
         NOLABELS
            MEAN
            MIN
            MAX
            NONOBS
         ;
         VAR &vars;
      RUN;
   %end;
   %else
   %if (&proc eq FREQ) %then %do;
      PROC FREQ DATA=&data ORDER=INTERNAL;
         TABLES &vars /  SCORES=TABLE;
      RUN;
   %end;
   %else
   %if (&proc eq PRINT) %then %do;
      PROC PRINT DATA=&data;
         var &vars;
      RUN;
   %end;
%mend;
%loop_control(control=metadata)

=======================================================================

* error checking ;
%macro code;
   %put _local_;
%mend;
%loop_control(control=doesnotexist)

-----------------------------------------------------------------------
Notes:

The child macro "%code" must be created at run time before calling
this macro.

A local macro variable will be created whose name matches the name
of every variable in the control dataset.  The child macro is
responsible for referencing the macro variables by the correct name.

The child macro will be called for each logical observation that is
read from the control dataset.

There is no need to globalize any of the macro variables, since the
names are "reused" during each iteration.  If you REALLY need a
"macro array" of macro variables (i.e. MVAR1, MVAR2, MVAR3, etc),
you should use a different approach, rather than trying to force
this macro to do this for you.

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

%macro loop_control
/*---------------------------------------------------------------------
A "wrapper" macro to execute code over a list of items defined in a
control table
---------------------------------------------------------------------*/
(CONTROL=      /* Control table defining the data used by the child  */
               /* macro (REQ).                                       */
,MNAME=code    /* Macro name (REQ).  Default is "%code"              */
);

%local macro parmerr __iter__ _data_;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(CONTROL,      _req=1,_words=1,_case=N)  /* words allows ds options */
%parmv(MNAME,        _req=1,_words=0,_case=N)

%if (&parmerr) %then %goto quit;

%* additional error checking ;

%* parse off any data set options ;
%let _data_=%scan(&control,1,%str( %());

%* does the control table exist? ;
%if not (%sysfunc(exist(&_data_,data)) or %sysfunc(exist(&_data_,view))) %then %do;
   %parmv(_msg=The &control dataset or view does not exist)
   %goto quit;
%end;

%* open control table for input ;
%let dsid=%sysfunc(open(&control,i));

%* if unable to open the control table abort ;
%if (not &dsid) %then %do;
   %parmv(_msg=Unable to open &control for input)
   %goto quit;
%end;

%* create a local macro variable for each variable in the PDV ;
%* the macro variable name will be the same as the dataset variable name ;
%syscall set(dsid);

%* iterate over the control dataset, calling the child macro for each record ;
%* the macro variable names in the child macro must match the variable names in the control dataset ;
%let __iter__ = 1;
%do %while (%sysfunc(fetch(&dsid)) eq 0);
%&mname
%let __iter__ = %eval(&__iter__+1);
%end;

%* close the open control table ;
%let dsid=%sysfunc(close(&dsid));

%quit:

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : splitvar.sas
Purpose                 : In-datastep macro to insert split characters
                          in a string variable.
SAS Version             : SAS 9.1.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 11FEB2011
Program Version #       : 1.1

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

Based on original work by Roland Rashleigh-Berry.
http://www.datasavantconsulting.com/roland/splitvar.sas
Used with permission.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 28MAR2011
Change/reason           : Detect when splits have already been put into
                          lines, and add addional ones.
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

options ls=65;
%let options=%sysfunc(getoption(ls,keyword)) %sysfunc(getoption(center,keyword));

data source;
   length string $600 line1 line2 line3 $200;
   infile datalines truncover;
   input line1 $char200. / line2 $char200. / line3 $char200.;
   string=catt(of line:);
   drop line:;
   datalines;
This is a long string that is much longer than the current
 linesize.  We want to insert a split character when we encounter
 a space or a hyphen that is near the end of the line.
;
run;

data report;
   set source;
   %splitvar(string,%sysfunc(getoption(ls)),outvar=newstr1,split=~);
   %splitvar(string,%sysfunc(getoption(ls)),outvar=newstr2,split=~,hanging_indent=5);
   %splitvar(string,40,outvar=newstr3,split=~);
run;

options ls=max nocenter;

proc print data=report;
run;

proc report data=report missing nowd headline headskip split="~" ls=80 spacing=2;
   column string newstr1 newstr2 newstr3 newstr3=newstr4;
   define string        / display width=65   flow;  * implicit flowing original string w/in linesize ;
   define newstr1       / display width=65   flow;  * explicit flowing new string w/in linesize ;
   define newstr2       / display width=70   flow spacing=2; * explicit flowing new string w/in linesize with hanging indent ;
   define newstr3       / display width=40   flow;  * width equal to explicit flow width ;
   define newstr4       / display width=30   flow;  * width smaller than explicit flow width for illustration ;
run;

options &options;

-----------------------------------------------------------------------
Notes:

A split character will normally be placed in a blank space. If there is
no suitable space then it will be inserted after a hyphen. But if there
is no suitable space and no hyphen then it will be inserted at the end.
You must ensure there is enough room to do this by ensuring the length
of the variable is greater than the length of any string.

This macro will only look back the floor of half the column width to
find a place to insert the split character.

If the outvar parameter is not specified, the input variable is
altered in place.

The default split character is ~.  The split character should not be
present in the input string.

No error checking is done to ensure the col parameter is less than the
current linesize.

Note that this is not a function-style macro. It must be used in a data
step as shown in the usage notes.

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

%macro splitvar
/*---------------------------------------------------------------------
Macro to insert split characters in a string variable
---------------------------------------------------------------------*/
(VAR           /* Input variable name (REQ).                         */
               /* Only one variable may be specified.                */
,COLS          /* Column number at which splitting should occur (REQ)*/
,SPLIT=~       /* Split character (REQ).                             */
               /* Must be a single character.                        */
,OUTVAR=       /* Output variable name (Opt).                        */
               /* If not specified, the input variable is altered.   */
,HANGING_INDENT=0
               /* If a hanging indent is included, then this is      */
               /* added after every new split.                       */
);

%local macro parmerr drop rename hi;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(VAR,        _req=1,_words=0,_case=N)
%parmv(COLS,       _req=1,_words=1,_case=N)
%parmv(SPLIT,      _req=1,_words=0,_case=N)
%parmv(OUTVAR,     _req=0,_words=0,_case=N)

%if (&parmerr) %then %goto quit;

%* additional error checking ;
%* the split character must be a single character ;
%let split=%sysfunc(dequote(&split));  %* dequotes a properly quoted split character ;
%if (%length(&split) ne 1) %then
   %parmv(_msg=Split character &split is not a single character);

%if (&parmerr) %then %goto quit;

%* if hanging indent was specified, create the hanging indent prefix ;
%if (&hanging_indent) %then %let hi=%qsysfunc(putc(,$&hanging_indent..));

%* if outvar parameter was specified, copy the input variable ;
%* to the output variable, and modify the output variable instead. ;
%if (&outvar ne ) %then %do;
   &outvar=&var;
   %let var=&outvar;
%end;

_pos=0;
do while(length(substr(&var,_pos+1))>&cols);

   *- if there is a delimiter within the line, then break there ;
   if (0<findc(&var,"&split",'ti',_pos+1)<_pos + &cols) then do;
      _pos=findc(&var,"&split",'ti',_pos+1)+1;
      _cols=1;
   end;

   else do _cols=(&cols) to floor(&cols/2) by -1;
      if substr(&var,_pos+_cols,1) EQ ' ' then do;
         &var=substr(&var,1,_pos+_cols-1)||
              "&split&hi"||
              left(substr(&var,_pos+_cols+1));
*         _pos=_pos+_cols+&hanging_indent+1;
         _pos=_pos+_cols+1;
         _cols=1;
      end;
   end;

   *- if space character not found look for a hyphen - ;
   if _cols>1 then do;
      do _cols=&cols to floor(&cols/2) by -1;
         if substr(substr(&var,_pos+1),_cols,1) EQ '-' then do;
            &var=substr(&var,1,_pos+_cols)||
                "&split&hi"||
                left(substr(&var,_pos+_cols+1));
*            _pos=_pos+_cols+&hanging_indent+1;
            _pos=_pos+_cols+1;
            _cols=1;
         end;
      end;
   end;

   *- if no hyphen found then split at end - ;
   if _cols>1 then do;
      &var=substr(&var,1,_pos+&cols)||
           "&split&hi"||
           left(substr(&var,_pos+&cols+1));
*      _pos=_pos+_cols+&hanging_indent+1;
      _pos=_pos+&cols+1;
   end;
end;

%* fix bug in PROC REPORT flow algorithm ;
%* the last line of a multi-line flowed column has a leading space truncated ;
%* see http://support.sas.com/kb/5/845.html ;
/*
%if (&hanging_indent) %then %do;
   * fix bug in PROC REPORT flow algorithm ;
   _pos=findc(&var,"&split",'ti',-9999);
   if _pos then &var=catt(&var,"&split");
%end;
*/

drop _pos _cols;

%quit:

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : varlist2.sas
Purpose                 : Returns a string containing a space separated
                          list of variables in a dataset.  Supports
                          dataset options and sorting of the returned
                          variable list.
SAS Version             : 9.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 17NOV2016
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 07JUL2017
Change/reason           : Added UPCASE and SORT parameters
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

%varlist2(sashelp.shoes);
%put &=varlist;

   Returns the list of variables in SASHELP.SHOES,
   in PDV order.

=======================================================================

%varlist2(sashelp.shoes(keep=Region Subsidiary Sales Returns));
%put &=varlist;

%varlist2(sashelp.shoes(keep=Region--Stores));
%put &=varlist;

%varlist2(sashelp.shoes(keep=_character_));
%put &=varlist;

%varlist2(sashelp.shoes(keep=_numeric_));
%put &=varlist;

%varlist2(sashelp.shoes (drop=Region Subsidiary Sales Returns));
%put &=varlist;

%varlist2(sashelp.shoes (drop=Region--Stores));
%put &=varlist;

%varlist2(sashelp.shoes (drop=_character_));
%put &=varlist;

%varlist2(sashelp.shoes (drop=_numeric_));
%put &=varlist;

* nonsensical but works! ;
%varlist2(sashelp.shoes (drop=_all_));
%put &=varlist;

   Returns the list of variables in SASHELP.SHOES,
   in PDV order,
   with the dataset option applied.

=======================================================================

%varlist2(sashelp.shoes,upcase=Y);
%put &=varlist;

%varlist2(sashelp.shoes (keep=Region Subsidiary Sales Returns),upcase=Y);
%put &=varlist;

   Returns the list of variables in SASHELP.SHOES,
   in PDV order,
   with the dataset option applied,
   with the returned variable list uppercased.

=======================================================================

%varlist2(sashelp.shoes,sort=Y);
%put &=varlist;

%varlist2(sashelp.shoes (drop=Region--Subsidiary),sort=Y);
%put &=varlist;

   Returns the list of variables in SASHELP.SHOES,
   in sorted (alphabetical) order,
   with the dataset option applied.

=======================================================================

%varlist2(sashelp.shoes,upcase=Y,sort=Y);
%put &=varlist;

%varlist2(sashelp.shoes (drop=Region--Subsidiary),upcase=Y,sort=Y);
%put &=varlist;

   Returns the list of variables in SASHELP.SHOES,
   in sorted (alphabetical) order,
   with the dataset option applied,
   with the returned variable list uppercased.

=======================================================================

%varlist2(sashelp.shoes,mvar=mymvar);
%put &=mymvar;

%varlist2(sashelp.shoes,mvar=mymvar,upcase=Y);
%put &=mymvar;

%varlist2(sashelp.shoes,mvar=mymvar,upcase=Y,sort=Y);
%put &=mymvar;

   Returns the list of variables in SASHELP.SHOES,
   in the user-specified macro variable &MYMVAR.

-----------------------------------------------------------------------
Notes:

The (separate) %varlist macro is a function style macro that returns
the variables in a dataset.  However, that macro does not support
dataset options, particularly DROP or KEEP, nor does it support
returning the variable list in sorted (alphabetical) order.

This macro (%varlist2) is not a function style macro.  However, it
does support dataset options, particularly DROP or KEEP.

This macro (%varlist2) also supports returning the variable list in
sorted (alphabetical) order.

This macro creates the global macro variable VARLIST, which contains
the dataset variable list.  Specify the MVAR= parameter to specify
a different name.

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

%macro varlist2
/*---------------------------------------------------------------------
Returns a string containing a space separated list of variables in a
dataset.  Supports dataset options and sorting of the returned
variable list.
---------------------------------------------------------------------*/
(DATA          /* Source dataset (REQ).                              */
,UPCASE=N      /* Uppercase the returned variable names? (Opt).      */
               /* Default value is NO.  Valid values are:            */
               /* 0 1 OFF N NO F FALSE and ON Y YES T TRUE           */
               /* OFF N NO F FALSE and ON Y YES T TRUE               */
               /* (case insensitive) are acceptable aliases for      */
               /* 0 and 1 respectively.                              */
,SORT=N        /* Return the variable list in sorted (alphabetical)  */
               /* order? (Opt).                                      */
               /* Default value is NO.  Valid values are:            */
               /* 0 1 OFF N NO F FALSE and ON Y YES T TRUE           */
               /* OFF N NO F FALSE and ON Y YES T TRUE               */
               /* (case insensitive) are acceptable aliases for      */
               /* 0 and 1 respectively.                              */
,MVAR=VARLIST  /* Name of returned macro variable (REQ).             */
);

%local macro parmerr notes;

%* check input parameters ;
%let macro = &sysmacroname;
%parmv(DATA,         _req=1,_words=1,_case=N)
%parmv(UPCASE,       _req=0,_words=0,_case=U,_val=0 1)
%parmv(SORT,         _req=0,_words=0,_case=U,_val=0 1)
%parmv(MVAR,         _req=1,_words=0,_case=U)

%if (&parmerr) %then %goto quit;

%global &mvar;
%let &mvar=;

%* capture the current NOTES system option ;
%let notes=%sysfunc(getoption(notes));
options nonotes;

proc contents data=&data out=_contents_ (keep=name varnum) noprint;
run;
proc sql noprint;
   %if (&upcase) %then %do;
      select upcase(name)
   %end;
   %else %do;
      select name
   %end;
   into :&mvar separated by ' '
   from _contents_
   %if (&sort) %then %do;
      %if (&upcase) %then %do;
         order by upcase(name)
      %end;
      %else %do;
         order by name
      %end;
   %end;
   %else %do;
      order by varnum
   %end;
   ;
   drop table _contents_;
quit;

%* restore the NOTES system option ;
options &notes;

%quit:
%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : bench.sas
Purpose                 : Measures elapsed time between successive
                          invocations.
SAS Version             : SAS 8.2
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 24APR2006
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 09JUL2015
Change/reason           : Added DATA, MESSAGE, and PRINT parameters
                          to capture running metrics.
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

* Start benchmarking.
* Both invocations are identical as long as start ;
* has not been previously invoked ;
%bench;
%bench(start);

%let rc=%sysfunc(sleep(3));

* Get elapsed time, should be approx. 3 seconds elapsed, 3 seconds total ;
%bench(elapsed);

%let rc=%sysfunc(sleep(7));

* Get another elapsed time, should be approx. 7 seconds elapsed, 10 seconds total ;
%bench;  * elapsed parm not required since start was already called ;

%let rc=%sysfunc(sleep(2));

* End benchmarking, should be approx. 2 seconds elapsed, 12 seconds total ;
* Must be called after start.  Resets benchmarking. ;
%bench(end);

* Capture running metrics ;
%bench(start,data=metrics);  * this will delete work.metrics if it exists ;
%let rc=%sysfunc(sleep(3));
%bench(elapsed,data=metrics,message=Option 1); * add current metrics to work.metrics ;
%let rc=%sysfunc(sleep(5));
%bench(elapsed,data=metrics,message=Option 2xxx);
%let rc=%sysfunc(sleep(11));
%bench(elapsed,data=metrics,message=Option 3yyyyy);
%bench(end,data=metrics,print=Y);  * prints default metrics output to log ;

-----------------------------------------------------------------------
Notes:

If %bench has never been invoked, calling %bench without parameters
starts benchmarking.  You may also explicity specify the start
parameter.  Explicitly specifying the start parameter resets
benchmarking, although normally the end parameter would be used.

If %bench has been previously invoked with the start parameter, calling
%bench without parameters prints the elapsed time.  You may also
explicity specify the elapsed parameter.

To end benchmarking and reset the start time, specify the end
parameter.

Only the elapsed or end parameters (or equivalent processing) print
time measurements to the log.  The start parameter does not print
anything to the log.

The only parameter that needs to be explicitly specified is end.
Otherwise the macro should do the right thing, either starting
benchmarking or printing elapsed times.

Benchmarking a time period greater than 24 hours is "unpredictable".

If you don't like the default metrics output, specify
%bench(end); or %bench(end,print=N);, then write your own output
using the metrics dataset that was created.

There may be very slight differences between the output written to the
log and the output captured in the metrics dataset.  This is likely
due to precision differences between the %sysevalf macro function and
the data step.

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

%macro bench
/*---------------------------------------------------------------------
Measures elapsed time between successive invocations.
---------------------------------------------------------------------*/
(PARM          /* Benchmarking parameter (Opt).                      */
               /* If not specified:                                  */
               /*    If first invocation, start benchmarking.        */
               /*    If subsequent invocation, print elapsed time.   */
               /* Valid values are START ELAPSED END.                */
,DATA=         /* Output dataset for running metrics (Opt).          */
               /* If specified, the &_START and &_ELAPSED data are   */
               /* written out to the specified dataset.              */
,MESSAGE=      /* Message to include with running metrics (Opt).     */
               /* If specified, the message is included with the     */
               /* running metrics.  This parameter is ignored if     */
               /* DATA= is not specified.                            */
,PRINT=N       /* Print default metrics to log? (Opt).               */
,DELETE=Y      /* Delete output dataset for running metrics? (Opt)   */
);

%local macro parmerr time_elapsed time_total time_elapsed_str time_total_str h m s;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(PARM,         _req=0,_words=0,_case=U,_val=START ELAPSED END)
%parmv(DATA,         _req=0,_words=0,_case=N)
%parmv(MESSAGE,      _req=0,_words=1,_case=N)
%parmv(PRINT,        _req=0,_words=0,_case=U,_val=0 1)
%parmv(DELETE,       _req=0,_words=0,_case=U,_val=0 1)

%if (&parmerr) %then %return;

%* if print=Y then data must be specified ;
%if (&print and &data eq ) %then %do;
   %parmv(_msg=If PRINT=Y then DATA must be specified)
   %return;
%end;

%* nested macro for printing ;
%macro print(_parm);
   %let time_elapsed       = %sysevalf(%sysfunc(datetime()) - &_elapsed);
   %let time_total         = %sysevalf(%sysfunc(datetime()) - &_start);

   %let h                  = %sysfunc(hour(&time_elapsed),z2.);
   %let m                  = %sysfunc(minute(&time_elapsed),z2.);
   %let s                  = %sysfunc(second(&time_elapsed),z2.);
   %let time_elapsed_str   = &h hours, &m minutes, &s seconds;

   %let h                  = %sysfunc(hour(&time_total),z2.);
   %let m                  = %sysfunc(minute(&time_total),z2.);
   %let s                  = %sysfunc(second(&time_total),z2.);
   %let time_total_str     = &h hours, &m minutes, &s seconds;

   %put;
   %put Benchmark &_parm:;
   %put;
   %put Elapsed time: &time_elapsed_str &time_elapsed;
   %put Total   time: &time_total_str &time_total;
   %put;
%mend;

%* declare global variables ;
%global _start _elapsed;

%if (&parm eq START) %then %do;
   %let _start    = %sysfunc(datetime());
   %let _elapsed  = &_start;

   %* If DATA= was specified then delete existing dataset ;
   %if (&delete and &data ne ) %then %do;
      %if (%sysfunc(exist(&data))) %then %do;
         proc delete data=&data;
         run;
      %end;
   %end;
%end;
%else
%if (&parm eq ELAPSED) %then %do;
   %if (&_start eq ) %then %do;
      %put ERROR:  Benchmarking must be started before elapsed time can be printed.;
      %return;
   %end;
   %else %do;
      %print(ELAPSED);
      %let _elapsed  = %sysfunc(datetime());

      %* If DATA= was specified capture running metrics ;
      %if (&data ne ) %then %do;
         %if (%sysfunc(exist(&data))) %then %do;
            data &data;
               set &data end=eof;
               output;
               if eof then do;
                  %* do not reorder these lines ;
                  message="&message";
                  start=&_start;
                  elapsed=&_elapsed-current;
                  current=&_elapsed;
                  total=total+elapsed;
                  output;
               end;
            run;
         %end;
         %else %do;
            data &data;
               length  message $200 start current elapsed total 8;
               message="&message";
               start=&_start;
               current=&_elapsed;
               elapsed=current-start;
               total=elapsed;
               format start current datetime.;
            run;
         %end;
      %end;
   %end;
%end;
%else
%if (&parm eq END) %then %do;
   %if (&_start eq ) %then %do;
      %put ERROR:  Benchmarking must be started before elapsed time can be printed.;
      %return;
   %end;
   %else %do;
      %print(END)

      %* reset benchmarking ;
      %symdel _start _elapsed / nowarn;

      %* if PRINT=Y then print default metrics to log ;
      %if (&print) %then %do;
         %let ls=%sysfunc(getoption(ls));
         options ls=max;
         data _null_;
            set &data end=eof;
            retain maxlen;
            maxlen=max(length(strip(message)),maxlen);
            if eof then call symputx("maxlen",maxlen,"L");
         run;
         data _null_;
            set &data end=eof;
            file log;
            if _n_=1 then
            put
               @1 "Benchmark Metrics:"
               /
               @1 "=================="
            ;
            put
               @1 message $&maxlen..
               +2 "Elapsed:" elapsed 12.4-R
               +2 "Total:"   total   12.4-R
            ;
            if eof then put "0A0D"x @;
         run;
         options ls=&ls;
      %end;
   %end;
%end;
%else
%if (&parm eq ) %then %do;
   %* derive proper parm then recursively call this macro ;
   %if (&_start eq ) %then %do;
      %bench(start);
   %end;
   %else %do;
      %bench(elapsed);
   %end;
%end;

%mend;

/******* END OF FILE *******/