SUPPORT / SAMPLES & SAS NOTES
 

Support

Sample 25035: Utility routines for procedure-like macros

DetailsDownloadsAboutRate It

Utility routines for procedure-like macros

Contents: Purpose / Requirements / Usage / Details / See Also
PURPOSE:
The macros in xmacro.sas are intended to make it (relatively) easy to write macros that act much like SAS procedures with respect to:
  • error checking (%xchk...)
  • setting defaults (%xchk...)
  • using variable lists (%xvlist)
  • doing BY processing (%xbylist, %xdo_by)
REQUIREMENTS:
Only Base SAS software is required.
USAGE:
The XMACRO macro definitions should be saved, without alteration, to a file on your system. Follow the instructions in the Downloads tab of this Sample.

There are macros to facilitate debugging and testing, and there are improved versions of %SUBSTR, %SCAN, the MERGE statement, and the LAGn function. There's also %INDEXC with no X prefix.

Outline of use

You must call %XINIT before using the other x macros and you must call %XTERM before terminating the main macro. It is strongly recommended that you follow the outline below as closely as possible to get all the features to work as intended.

   * NO defaults in macro statement. put defaults in %xchk... instead;
   %macro whatever( data=, var=, freq=, weight=, by=, singular=,
                    options=, ...);

   ************** initialize xmacros, turn off notes ************;
   %xinit( whatever)

   ************** check EVERY argument ONCE ************;
   * %xchk macros also set defaults, do range checks, echo arguments;
   %xchkdata( data, _LAST_)
   %xchklist( var, _NUMERIC_)
   %xchklist( freq, , , 1, 1)
   %xchklist( weight, , , 1, 1)
   %xchklist( by)
   %xchknum( singular, 1e-7, 0<SINGULAR and SINGULAR<1)
   ...

   ************** process options if you got any **************;
   %let print=0;  * for example;
   %let nomiss=0; * for example;

   %let n=1;
   %let token=%qscan(&options,&n,%str( ));
   %do %while(&token^=);
      %let token=%qupcase(&token);
      %if %xsubstr(&token,1,5)=PRINT %then %let print=1; %else
      %if %xsubstr(&token,1,6)=NOMISS %then %let nomiss=1; %else
      %xerrset(Unrecognized option &token);
      %let n=%eval(&n+1);
      %let token=%qscan(&options,&n,%str( ));
   %end;

   %xbug(Options:, print nomiss)

   ************** process BY variables ************;
   %xbylist;
   %xvlist( data=&data, _list=by, _name=by, valid=0123)

   ************** dummy data step to check for errors ***************;
   %xchkvar( &data, &by, &var &freq &weight)
   %if &_xrc_^=OK %then %goto chkend;

   ************** process FREQ= variable ***************;
   %xvfreq( &data)

   ************** process WEIGHT= variable ***************;
   %xvweight( &data)

   ************** process VAR= list ***************;
   %let remove=;
   %if %qupcase(&var)=_NUMERIC_ %then %do;
      %if %bquote(&by)^= %then %let remove=&remove by;
      %if %bquote(&freq)^= %then %let remove=&remove freq;
      %if %bquote(&weight)^= %then %let remove=&remove weight;
   %end;
   %xvlist( data=&data, _list=var, _name=var, _type=type, _count=nvar,
           remove=&remove, replace=1)

   ************** set more defaults if there are any ***********;
   ...

   ************** echo arguments with defaults set and
                  check for no observations in the data set **********;
%chkend:
   %xchkend(&data)
   %if &_xrc_^=OK %then %goto exit;

   ************** turn notes back on before creating
                  the real output data sets ****************;
   %xnotes(1)

   ************** do something useful here ***************;
   ...
   ...
   ...

   ************** check for errors after each
                  DATA or PROC step *******************;
   %if &syserr>4 %then %do;

      * Assign descriptive message to &_xrc_ (which %xterm checks)
        and print ERROR: message;
      %xerrset(Something went wrong while the WHATEVER macro was
               trying to do something useful);

   %end;

%exit:

   ************** issue termination message, reset notes and _LAST_ **;
   %xterm;

   %mend whatever;
DETAILS:
Xmain macro

The %xmain macro enables use of the &_test_ global variable for labeling test output. %xmain generates a %macro statement and a call to %xinit. You can use the following statement before including the main macro to automatically generate TITLEn statements to label test output:

   %let _test_=&line;    %* Defines a title line containing the
                            macro invocation, where 1<=&line<=10;

For the example above, the %xmain macro would be used like this:

   %unquote(%xmain(
      whatever( data=, var=, freq=, weight=, by=, singular=,
                    options=, ...)
   ))

   ************** check EVERY argument ONCE ************;
   etc.

A test job would start like this:

   title "Test WHATEVER macro";
   %let _test_=2; %* echoes macro invocation in title2;
   %include 'whatever.sas';

This works by specifying the PARMBUFF option in the main macro statement. You should not hard-code the PARMBUFF option because of a bug that prevents the macro processor from checking the arguments to the main macro when it is invoked--in other words, if the user makes a typo in one of the argument names, there will be no error message.

Checking for inclusion of xmacro

Unless you copy the xmacro macros that you need into your macro file, it's a good idea to check whether xmacro has been included. This is especially important if you use xmain, otherwise you can get vast quantities of error messages. You can put a macro like this at the beginning of your macro file:

%* checks whether 6.10 version of xmacro has been included; %macro xmacinc; %global _xmacro_; %if %bquote(&amp;_xmacro_)= %then %put ERROR: XMACRO has NOT been included.; %else %if %bquote(&amp;_xmacro_)=done %then %do; %let _xmacro_=; %put %qcmpres(ERROR: The 6.09 version of XMACRO has been included but the 6.10 or later version is required.); %end; %if %bquote(&amp;_xmacro_)= %then %do; %put NOTE: The WHATEVER macro will not be compiled.; %* comment out the rest of this file; %unquote(%str(/)%str(*)) %end; %mend xmacinc; %xmacinc;

If xmacro has not been included, the above macro invocation begins a slash-star comment. You will also need to include a macro comment containing a star-slash at the end of your macro file like this:

  %* close comment possibly generated by xmacinc *_/;

except, of course, that the _ between the * and / should be deleted. The _ is there to keep from terminating the comment that contains this text.

Never use slash-star comments in macros.

Debugging

The following statements in open code may be useful for debugging:

   %let _notes_=1;       %* Prints SAS notes for all steps;
   %let _echo_=1;        %* Prints the arguments to the main macro;
   %let _echo_=2;        %* Prints the arguments to the main macro
                            again after defaults have been set;
   %let _debug_=1;       %* Prints debugging information from non-x
                            macros;
   %let _xdebug_=1;      %* Prints debugging information from xmacros;
   options mprint;       %* Prints SAS code generated by the macro
                            language;
   options mlogic macrogen symbolgen;
                         %* Prints lots of macro debugging info;

To turn on all diagnostic information, use %XNOISY. To turn it back off, use %XQUIET. These can be called in open code.

In your macro, Use the &_DEBUG_ variable to determine whether to print debugging information. The %XBUG macro is handy for printing the values of many macro variables conditional on &_DEBUG_. The %XBUGDO macro generates SAS code conditional on &_DEBUG_.

To speed things up, use:

   %let _check_=1;       %* Supresses checks for excessively large
                            integers and for non-existent data sets
                            and libraries;
   %let _check_=0;       %* Supresses most argument checking;

If your macro can do extra, time-consuming error checking, make the extra error checks conditional on &_CHECK_ being greater than 1.

Argument checking

The %XCHK... macros are for checking and echoing the arguments to the main macro. You should generally call an %XCHK... macro for every argument before you do anything else with the argument, so that dangerous values will be detected. Users often forget the = sign for a keyword argument, so most of the %XCHK... macros check for extraneous ='s and extra tokens. Assiduous use of the %XCHK... macros greatly reduces the number of inscrutable error messages that users get.

Each %XCHK.. macro echoes the argument if &_ECHO_>=1. After checking all the arguments to the main macro and setting defaults, use %XCHKEND to echo the arguments again if &_ECHO_>=2.

   %XCHKDEF  Sets default value. Use this if no other
             checking is possible.
   %XCHKEQ   Issues error if argument contains an equals (=) sign.
   %XCHKONE  Issues error if argument has more than one token or
             contains an equals (=) sign.
   %XCHKUINT Checks an unsigned integer argument.
   %XCHKINT  Checks an (optionally signed) integer argument.
   %XCHKNUM  Checks a numeric (integer or floating point) argument.
   %XCHKMISS Checks a numeric or missing argument.
   %XCHKNAME Checks a SAS name.
   %XCHKDSN  Checks a data set name.
   %XCHKDATA Checks an input data set name.
   %XCHKKEY  Checks a value from a list of key words or values.
   %XCHKLIST Checks a list of integers, names, quoted strings,
             or special characters.
   %XCHKEND  Checks if a data set is empty, echoes all previously
             checked arguments if &_ECHO_>=2.

You should not call any of the above macros more than once for a single argument, since that would cause %XCHKEND to echo the argument more than once. However, in addition to checking arguments individually, you can call %XCHKVAR to check an input data set to see if variables from one or more lists are in the data set:

   %XCHKVAR  Runs a dummy DATA step to check if variables exist. Does
             not echo anything.

All of the %XCHK... macros except %XCHKEND and %XCHKVAR return the argument that is checked in quoted form.

Macro quoting

Macro quoting is an arcane art that requires much study and dedication to master. The basic rules (which, of course, have exceptions) are:

  1. Quote every macro variable before doing any character comparisons on it or using it as an argument to a macro.
  2. Unquote every macro variable before using it to generate code.

A corollary of (1) is:

 1a. Always use the Q... or X... versions of %SUBSTR, %UPCASE, %TRIM,
     %LEFT, etc.

The main purpose of quoting is to prevent an implicit %EVAL from going berserk. Implicit %EVALs occur in the condition of an %IF statement, various parts of a %DO statement, and some arguments to %(Q)SCAN and %(Q)SUBSTR. The trouble is that %EVAL evaluates _everything_ in an expression. So, for example,

   %IF &A=&B %THEN ...

doesn't simply do a character comparison of &A and &B, but tries to interpret each of those values as an expression. If either &A or &B contains an operator such as AND or -, %EVAL will try do an operation. If &A is something like X1-X99, %EVAL will try to subtract two character strings and get very upset. For example:

   %LET A=AND;
   %LET B=%EVAL(&A=OR);
ERROR: A character operand was found in the %EVAL function or %IF
       condition where a numeric operand is required. The condition
       was: and=or

The circumvention is:

   %LET B=%EVAL(%NRBQUOTE(&A)=%STR(OR));

%NRBQUOTE quotes the value &A at execution time. %STR quotes the literal OR at compile time. If the first operand were &&&A, then you would have to use %BQUOTE instead of %NRBQUOTE, since the latter would not rescan (hence the NR in the name) the argument.

Quoting is also needed when a positional argument to a macro contains an =, since the macro processor may misinterpret it as a keyword argument.

Quoting turns operators and other special characters into different, nonprintable characters. Quoting can also do funny things to tokenization. When you generate code from a macro variable, it's supposed to be automatically unquoted, but that doesn't necessarily work. If you get weird characters in your generated code, you forgot to unquote something. If ordinary ‘’ or "" quoted strings in your generated code cause weird errors, you forgot to unquote something.

If you use the %&ABC construct, &ABC must be unquoted. For example:

   %macro hello(n); %do i=1 %to &n; %put Hello; %end; %mend;
   %let x=hello(2);
   %&x;
Hello
Hello

   %let x=%bquote(&x);
   %&x;
ERROR: %EVAL function has no expression to evaluate, or %IF statement
       has no condition.

You can do the %UNQUOTE in a separate statement:

   %let x=%unquote(&x);
   %&x;
Hello
Hello

or, if you prefer really bizarre code, you can use:

   %unquote(%str(%%)%unquote(&x))
Hello
Hello

Macro usage notes

Macros ordinarily return a single value. The macro language does not support call-by-address. So to return more than one value, you have to use call-by-name. That is, you pass the name of an existing macro variable that you want the value returned in. In the x macros, arguments for returning a value (or sometimes many values) by call-by-name have leading underscores in their names.

For example, suppose you want to write a macro called %REP to repeat an argument n times with blanks in between. You could write it to return a value like this:

   %macro rep(arg,n);            %* call by value;
      %local r i;                %* must declare local variables;
      %let r=&arg;
      %do i=2 %to &n;
         %let r=&r &arg;
      %end;
      &r                         %* returned value;
   %mend rep;

   %let string=abc;
   %let repstr=%rep(&string,3);  %* call by value requires ampersand;
   %put repstr=&repstr;

repstr=abc abc abc

You could also write a macro to update a macro variable like this:

   %macro uprep(_arg,n);         %* underscore indicates call by name;
      %local __r __i;            %* must declare local variables and
                                    use double underscores for names;
      %let __r=&&&_arg;          %* reference to value of argument
                                    passed by name requires three
                                    ampersands;
      %do __i=2 %to &n;
         %let __r=&__r &&&_arg;
      %end;
      %let &_arg=&__r;           %* to assign a value to an argument
                                    passed by name requires one
                                    ampersand on the left side;
   %mend uprep;

   %let string=abc;
   %uprep(string,3);             %* call by name requires no ampersand;
   %put string=&string;

string=abc abc abc

Be careful to avoid conflicts between the name of an argument passed by call-by-name and local macro variables or argumemt names. Macros that take call-by-name should use names for local variables beginning with 2 underscores.

The macro language does not support arrays. Instead of arrays, names are used that are composed of a constant prefix and a varying numerical suffix. If an array needs to be returned by a macro, the caller provides the prefix, and the macro constructs the names and declares them global.

To reduce the chances of name conflicts, local variables should always be declared in a %LOCAL statement. The names of global variables should usually begin with an underscore, but this hasn't been done everywhere yet. Global variables that users should know about should begin and end with an underscore. Other prefix naming conventions may be needed.

The macro language cannot do floating point operations. It does not issue an error message if you try to do a floating point comparison, but the comparison may be incorrect. You can use %XFLOAT to run a DATA step to evaluate a floating point expression.

Every line of SAS code that the macro language generates is saved by the SAS supervisor in a utility file (the "spool" file) in the WORK library with an overhead of about 100 bytes per line. Therefore, you should try to minimize the number of lines of SAS code generated in order to postpone running out of disk space. In batch mode, this utility file is automatically reinitialized after each step, so it's usually not a problem except with IML (in IML, the EXECUTE, PUSH, and QUEUE statements add stuff to this utility file). In line mode, OPTIONS NOSPOOL may prevent the utility file from growing until it uses up all your disk space, so this option is set by %XINIT. In DMS mode, OPTIONS NOSPOOL has no effect, and the user must issue a CLEAR RECALL command to reinitialize this utility file. If you actually run out of disk space, you should get a multiple-choice quiz that says something like this (this is from MVS):

   Out of disk space for library WORK while trying to reserve a page
   for member @T000000.UTILITY. Select:
   ...
   3. Clear source spooling/DMS recall buffers.
   ...

Number 3 is the correct answer.

SAS usage notes

&SYSERR should be checked after all DATA and PROC steps that can fail in a way that damages subsequent steps.

Variables created in DATA steps or IML that might conflict with names of the user's variables should have names beginning with underscores.

In order to support OPTIONS FIRSTOBS= and OBS= and data set options on the DATA= data set, the following restrictions must be imposed on code generated by macros:

  • NEVER use data set options on the DATA= data set.
  • ALWAYS use FIRSTOBS= and OBS= data set options on all other input data sets, usually (firstobs=1 obs=2000000000)

Weird things you should know

  • The maximum allowed length of a macro variable is 32767 bytes. The documentation about this is wrong.
  • %INCLUDE is NOT a macro statement. So if you code:
          %if %bquote(_xyz_)= %then %include 'xyz.sas';
    
    the semicolon terminates the %THEN statement, but the %INCLUDE statement is generated as regular SAS code WITHOUT a semicolon.
  • Quotes in non-macro comments outside of macros do not need to be matched. Quotes in macros or macro statements, including macro comments, need to be matched unless you use something like %STR(%').
  • %BQUOTE does not quote ampersands as and-operators.
  • %EVAL always evaluates digit-strings as integers, even if they are quoted.
  • %EVAL does not remove leading zeros.
  • The following are buggy and should be used with extreme care: Nested quoting functions PARMBUFF option on MACRO statement
SEE ALSO:
The %DISTANCE, %STDIZE, and %ICE macros require the XMACRO macros.



These sample files and code examples are provided by SAS Institute Inc. "as is" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. Recipients acknowledge and agree that SAS Institute shall not be liable for any damages whatsoever arising out of their use of this material. In addition, SAS Institute will provide no support for the materials contained herein.