BookmarkSubscribeRSS Feed
xxformat_com
Barite | Level 11

It is easy to find a macro where macro variables were not set explicitely as local leading to potential issues as you already know.

 

The readonly option of the %global statement could be used in the validation phase as a way to detect each time a global macro variable is updated.

This would be especially useful when including macro developed by other persons.

It would require a manual review as it is common to update an existing variable e.g %let mymacrovar=%upcase(&mymacrovar.);

 

But how do we get the list of all the global macro variables defined by the user in a dataset? Dictionary.macros lists all global macro variables included those of the SAS system. There is no user specific attribute in the dictionaries.

 

*temporary add on when checking for global macro variables;
proc sql;
    create table ref as
    select name
    from dictionary.macros
    where scope='GLOBAL'
      and name='TEST'; *would need to be able to select all the user defined macro var;
quit;


data _null_;
   set ref;
   call execute ('%' || 'global  / readonly ' || name || ';');
run;

*Original code to be tested;
%let test=A;

%macro demo;
%let test=B;
%mend demo;
%demo;

 

4 REPLIES 4
LinusH
Tourmaline | Level 20

If I get it right, you don't want to prevent users for redefining a macro variable, but you wish to know when they do?

In your example, the assignment in the macro overrides the users global assignment, right?

I don't think that's something available OOTB.

Perhaps you can include logic in your "central" macros to check for this before the rest of the macro executes?

Data never sleeps
Quentin
Super User

Unfortunately, SAS doesn't really have a solid concept of 'user-created' global macro variables vs  other types.  If you look at scope in the dictionary table, there is an AUTOMATIC scope which has lots of system-created global macro variables.  But lots of other macro variables that are created automatically be SAS (e.g. SQLOBS) end up in the GLOBAL scope.  And many of the clients create tons of macro variables that are also in the global scope.

 

Worse yet, there is a critical flaw (IMHO) which makes /readonly macro variables dangerous to use.  Even if the macro developer appropriately uses the %LOCAL statement to instantiate a local macro variable, if there is a GLOBAL READONLY macro variable with the same name, the macro will throw an error.  Which is ridiculous, because the whole point of the %LOCAL statement is to prevent the collision with the global symbol table.

 

%global /readonly test=A;

%macro demo();
  %local test ;
  %let test=B;
%mend demo;
%demo()
The Boston Area SAS Users Group is hosting free webinars!
Next up: Joe Madden & Joseph Henry present Putting Power into the Hands of the Programmer with SAS Viya Workbench on Wednesday Nov 6.
Register now at https://www.basug.org/events.
Tom
Super User Tom
Super User

The late great Tom Hoffman made a macro to help with this 25 years ago.  %chkmvars().

The main goal of the macro is to test if the macro variables exist (this was before the %SYMEXIST() function was created).  But the second example in the usage notes shows how to use it to help with this topic.

 

Basically you would add a call to this %chkmvars() at the end of your macro to see if during the execution of the macro you had created any LOCAL macro variable that was not in the list of macro variables you expected to create as LOCAL.   You would basically use this during the development and testing of the macro to make sure your %LOCAL statements were sufficient. 

Example from header:

%macro testit(data=);
%local abc def ghi;

... other code ...

%chkmvars(data abc def ghi,mode=testit)
%if %length(&nomatch) %then
  %put Local macro variables &nomatch are not defined.
;
%mend testit;

Full code:

Spoiler
%macro chkmvars
/*----------------------------------------------------------------------
Checks list of macro variable against DICTIONARY.MACROS.

Returns either undefined variables or variables missing from a local
statement.
----------------------------------------------------------------------*/
(list         /* List of variables to check */
,mode=MISS    /* MISS - returns variables in list but not in dictionary
                 macroname - returns local variables not in list */
,mvar=nomatch /* Returned macro variable */
,global=0     /* Global=1 automatically adds any missed variables to
                 the global environmemt */
);

/*----------------------------------------------------------------------
This code was developed by HOFFMAN CONSULTING as part of a FREEWARE
macro tool set. Its use is restricted to current and former clients of
HOFFMAN CONSULTING as well as other professional colleagues. Questions
and suggestions may be sent to TRHoffman@sprynet.com.
-----------------------------------------------------------------------
Usage:

1) check to see whether any of the macro variables ABC DEF or GHI have
   not been previosuly defined:

%local momatch;
%chkmvars(abc def ghi,mode=miss)
%if %length(&nomatch) %then
  %put Macro variables &nomatch are not defined.
;

2) check to see whether any local variables used in a macro are not
   used as parameters or listed in the %local statement:

%macro testit(data=);
%local abc def ghi;

... other code ...

%chkmvars(data abc def ghi,mode=testit)
%if %length(&nomatch) %then
  %put Local macro variables &nomatch are not defined.
;
%mend testit;
-----------------------------------------------------------------------
Notes:

This tool is designed for macro developers. It serves two different
purposes depending upon the value of the MODE parameter:

mode=miss:

Invoke macro CHKMVARS before PARMV in a macro that depends upon the
existence of macro variables that are not parameters and are not
globally defined in a macro that initializes the programming environment.
The developer may choose to either localize or globalize the missing
macro variables or to issue an error message and bail out.

mode=macroname:

As a final check to determine whether the %local statement is complete,
invoke CHKMVARS at the bottom of the macro. Remove the macro invocation
once the %local statement is complete.
-----------------------------------------------------------------------
History:

12JUL99 TRHoffman Creation
----------------------------------------------------------------------*/
%local macro parmerr n word save;
%let macro=chkmvars;

%*----------------------------------------------------------------------
Validate macro parameters
-----------------------------------------------------------------------;
%parmv(list,_req=1,_words=1)
%parmv(mode,_req=1)
%parmv(mvar,_req=1)
%parmv(global,_req=1,_val=0 1)
%if (&parmerr) %then %goto quit;

%if ^%mvartest(&mvar) %then %do;
  %global &mvar;
%end;
%let &mvar =;

%*----------------------------------------------------------------------
Use the SQL insert command to create a table with one row for each macro
variable in the list parameter.

Compare with DICTIONARY.MACROS and write nonmatched variables to &mvar.
-----------------------------------------------------------------------;
%let save = %sysfunc(getoption(notes));
option nonotes;
proc sql noprint;
  create table _list_ (name char(8));
  insert into _list_
%do %until(&word =);
  %let n = %eval(&n + 1);
  %let word = %scan(&list,&n,%str( ));
  values("&word")
%end;
  ;
  select name into:&mvar separated by ' '

%if (&mode = MISS) %then %do;
  from _list_
  where name not in (select name from dictionary.macros)
%end;

%else %do;
  from dictionary.macros
  where scope = "&mode" and
   name not in (select name from _list_)
%end;
;
  drop table _list_;
quit;
option &save;

%if (&global) & %length(&&&mvar) %then
  %global &&&mvar
; ;

%quit:
%mend chkmvars;

 

 

Quentin
Super User

Tom's answer reminds me of another approach to this problem. Frank DiIorio wrote a macro %WhatChanged() which is designed to detect side-effects of macros (e.g. changes to global macro variables, or changes to system options, or other global stuff).  https://www.lexjansen.com/nesug/nesug10/bb/bb02.pdf

 

You would use %WhatChanged() in a test script for a macro, like (untested):

%macro demo();
  %*oops forgot %local statement;
  %let test=B;
%mend demo;

%global A ; %whatChanged(action=start) %demo() %whatChanged(action=end)

The limitation of this approach for detecting macro variable collisions is that in the test script, you have to declare the names of all the macro variables that you want to check for potential collisions. So if you forgot to list a macro variable on your %local statement, you might also forget to list it in your test script. But since %WhatChanged also checks for other side-effects, I'm a fan of the approach.

The Boston Area SAS Users Group is hosting free webinars!
Next up: Joe Madden & Joseph Henry present Putting Power into the Hands of the Programmer with SAS Viya Workbench on Wednesday Nov 6.
Register now at https://www.basug.org/events.

SAS Innovate 2025: Save the Date

 SAS Innovate 2025 is scheduled for May 6-9 in Orlando, FL. Sign up to be first to learn about the agenda and registration!

Save the date!

How to Concatenate Values

Learn how use the CAT functions in SAS to join values from multiple variables into a single value.

Find more tutorials on the SAS Users YouTube channel.

SAS Training: Just a Click Away

 Ready to level-up your skills? Choose your own adventure.

Browse our catalog!

Discussion stats
  • 4 replies
  • 715 views
  • 4 likes
  • 4 in conversation