BookmarkSubscribeRSS Feed
☑ This topic is solved. Need further help from the community? Please sign in and ask a new question.
RichardAD
Quartz | Level 8

Is there some mechanism that can determine if a macro was defined with certain argument list ?

MacSignatureMatch(<macro-name>, <argument-list>)

would return 0 for no, and 1 for yes

 

Example use:

 

Suppose we have two macros defined as

%macro xyzzy (x, y, z, foo=, bar=) ;
...
%mend xyzzy ;

%macro snafu (x, y, z, foo=, bar=) ; ... %mend snafu ;

And they are available for a program to use via a parameter specification

%let wedge_postmacro = SNAFU ;

The program will code generate the macro call based on control data supplied

proc import dbms=xlsx datafile=control replace out=control ;
  sheet = 'dispatches' ;
run ;
* validate structure and contents of control... ;

Further suppose the control data has a column [program] for which after including the program, the specified macro needs to be invoked, but only if the macro call will not cause a macro invocation error.

data _null_ ;
  set control_validated ;
  call execute ('%nrstr(%include) ' || quote(trim(program)) || ' ;') ;
  parameter = 'wedge_postmacro' ;
  if symexist(parameter) then do ;
    macro = symget(parameter) ;
    if not missing (macro) then do ;
      exists = resolve ('%sysmacexist(' || trim(macro) || ')') ;
      if exists then do ;
*** ;
*** ; Can signature be deteremined ?
*** ; For sake of argument suppose there is an FCMP function named MacSignatureMatch() that can do so ;
*** ;
if MacSignatureMatch(macro, 'x, y, z, foo=, bar=') then do ;
call execute ('%nrstr(' || trim(macro) || ')'
|| '( ' || cats(controlX)
|| ', ' || cats(controlY)
|| ', ' || cats(controlZ)
|| ', foo=' || cats(controlFoo)
|| ', bar=' || cats(controlBar)
end ;
      end ;
    end ;
  end ;
run ;

  The wedge macro itself may cause warnings or errors if coded incorrectly for the wedge task at hand.

1 ACCEPTED SOLUTION

Accepted Solutions
Tom
Super User Tom
Super User

With a little more experimentation this seems to work very well.

data stored_macros ;
  length macname $32 order 8 parmname $32 call_by_position 8 default $200 ;
  set sashelp.vcatalg;
  where libname='WORK' and objtype='MACRO';
  macname=objname;
  fname=catx('.',libname,memname,objname,objtype);
  infile sasmacr catalog filevar=fname truncover length=ll dsd dlm='00'x end=eof;
  order=0;
  do while(not eof);
    input @;
    if _infile_=: '02'x then do;
      order+1;
      input @5 call_by_position pib1. @9 parmname @45 default @;
      output;
    end;
    input;
    if _infile_='04000000'x then leave;
  end;
  if order=0 then output;
run;

So I I define the macro %MYMAC to have two parameters that can be passed by POSITION and two that required the values to passed by NAME like this:

%macro mymac(in,out,named=,named2=default value); %mend;

And run the data step above I get these observations:

Tom_0-1737766910000.png

 

 

View solution in original post

17 REPLIES 17
Tom
Super User Tom
Super User

No.

You will need to build your own.  You could build your own code parser to check the .sas file(s) that contain the macro definitions and figure it out.

Or better still build a metadata table that you manually keep in synch with your macro definitions that you could query.

Quentin
Super User

I wonder if you could hack it by invoking the macro with the candidate arguments, then parse the log to see which are not defined.

 

So given macro:

%macro foo(x=,y=) ;
%mend foo ;

You could test it with a macro:

%macro MacSignatureMatch(macro=,argumentlist= /*space-delimited list*/) ;
  %*convert space delimited argument list to comma delimited keyword parameters;
  %* using Richards excellent %seplist macro;
  %* https://www.devenezia.com/downloads/sas/macros/index.php?m=seplist ;
  %let argumentlist=%seplist(&argumentlist,dlm=%str(,),suffix==) ;

  %&macro(&argumentlist,FakeArgument=)
 
%mend  MacSignatureMatch ;

The FakeArgument is added so that the macro invocation always fails.

 

If all candidate arguments are defined for the macro, the log should report only that FAKEagrument was not found:

71   %MacSignatureMatch(macro=foo,argumentlist=x y)
ERROR: The keyword parameter FAKEARGUMENT was not defined with the macro.

If an argument from argumentlist is not defined, it will be reported in the log:

72   %MacSignatureMatch(macro=foo,argumentlist=x y z)
ERROR: The keyword parameter Z was not defined with the macro.
ERROR: The keyword parameter FAKEARGUMENT was not defined with the macro.
The Boston Area SAS Users Group (BASUG) is hosting an in person Meeting & Training on June 27!
Full details and registration info at https://www.basug.org/events.
webart999ARM
Quartz | Level 8

To determine if a macro expects specific arguments (its signature) in SAS, you can use a custom macro that retrieves the macro's source code and parses its parameter list. Try this:

Step-by-Step Explanation

  1. Check Macro Existence: Use %sysmacexist to verify the macro exists.

  2. Retrieve Macro Source: Use %copy to access the macro's source code.

  3. Parse Parameters: Extract the parameter list from the %macro statement, handling multi-line definitions and comments.

  4. Compare Signatures: Clean and compare the extracted parameters against the expected signature.

%macro MacSignatureCheck(macroname, expected_params);
  %local fileref params expected_clean rc start end;
  %let fileref = %sysfunc(tempfilename(fn));

  /* Clean expected parameters: uppercase and remove spaces */
  %let expected_clean = %sysfunc(compress(%upcase(&expected_params), %str( )));

  /* Attempt to copy macro source */
  %let rc = %sysfunc(filename(fileref, &fileref));
  %let copystat = %sysfunc(copy(&macroname, &fileref, SOURCE));
  %if &copystat ne 0 %then %do;
    %put WARNING: Could not copy macro &macroname source.;
    filename &fileref clear;
    %return(0);
  %end;

  /* Read and parse macro source to extract parameters */
  data _null_;
    infile &fileref lrecl=32767 truncover;
    length combined $32767 temp $32767 line $32767;
    retain combined '';
    input line $char32767.;
    combined = cat(strip(combined), strip(line));

    /* Continue reading until closing ')' if needed */
    if index(combined, '(') and not index(combined, ')') then do until (eof); 
      do until (index(temp, ')') or eof);
        input line $char32767.;
        if _error_ then eof = 1;
        temp = cat(strip(temp), strip(line));
      end;
      combined = cat(strip(combined), strip(temp));
    end;

    /* Remove comments */
    combined = prxchange('s/\/\*.*?\*\///', -1, combined);

    /* Extract parameters from %MACRO statement */
    start = index(upcase(combined), '(');
    end = index(upcase(combined), ')');
    if start and end then do;
      params = substr(combined, start+1, end - start -1);
      params = compress(upcase(params), ' '); /* Remove spaces */
      call symputx('params', params);
    end;
  run;

  /* Determine result and clean up */
  filename &fileref clear;
  %if "%superq(params)" = "&expected_clean" %then 1;
  %else 0;
%mend MacSignatureCheck;

/* Example usage in data step */
data _null_;
  set control_validated;
  call execute('%nrstr(%include) ' || quote(trim(program)) || ';');
  parameter = 'wedge_postmacro';
  if symexist(parameter) then do;
    macro = symget(parameter);
    if not missing(macro) then do;
      exists = resolve('%sysmacexist(' || trim(macro) || ')');
      if exists then do;
        /* Check if signature matches */
        length result $1;
        result = resolve('%MacSignatureCheck(' || trim(macro) || ', %nrstr(x, y, z, foo=, bar=))');
        if result = '1' then do;
          call execute('%nrstr(%' || trim(macro) || 
            '(x=' || cats(controlX) || 
            ', y=' || cats(controlY) || 
            ', z=' || cats(controlZ) || 
            ', foo=' || cats(controlFoo) || 
            ', bar=' || cats(controlBar) || '));');
        end;
      end;
    end;
  end;
run;

Explanation

  1. Macro %MacSignatureCheck:

    • Copies Source: Uses %copy to retrieve the macro's source code.

    • Reads and Parses: Extracts the parameter list from the %macro statement, handling multi-line definitions and removing comments.

    • Compares Parameters: Cleans (uppercase, no spaces) and compares against the expected parameters.

  2. Data Step Integration:

    • Checks Existence: Uses %sysmacexist to verify the macro exists.

    • Calls %MacSignatureCheck: Uses resolve to dynamically check the macro's signature.

    • Generates Call: If the signature matches, uses call execute to generate the macro call safely.

This approach dynamically verifies a macro's signature before invocation, preventing potential errors from mismatched parameters.

 

Hope this helps somehow.

Tom
Super User Tom
Super User

Not sure how well that can work. 

 

I cannot find any TEMPFILENAME() function.

 

Using %SYSMACEXIST only works after the macro has been compiled.  So not useful for autocall macros.

 

Using %COPY does not seem that useful as it requires that you have the macros already COMPILED into a catalog.  AND that they all used the / SOURCE option on the %MACRO statement.  Again not useful for autocall macros.  How could you check what parameters the SAS supplied autocall macros like %LEFT() use?

 

If you did have a stored macro library and all of the macros were compiled with the /STORE option then it is probably going to be easier to just scan ALL of the macros in the catalog and make a dataset with the list of parameters.  Then you can check your database using simple data processing steps without any need for macro logic.

 

Similarly if you are using AUTOCALL macros just scan the directories included in the SASAUTOS system option for the %MACRO statements.  (Watch out if you scan the SAS supplied autocall macros as some of them have more than one macro definition in the same file.)

 

 

Quentin
Super User

@Tom wrote:

Not sure how well that can work. 

 

I cannot find any TEMPFILENAME() function.

...

Code using a non-existent function is likely a result of a ChatGPT hallucination.  Was the answer generated by ChatGPT or another LLM, @webart999ARM ? 

 

I would suggest if you post answers generated bv an LLM, it would be helpful to mention that in the post.  So people will know the code was generated (not written) and may contain hallucinations.  

The Boston Area SAS Users Group (BASUG) is hosting an in person Meeting & Training on June 27!
Full details and registration info at https://www.basug.org/events.
SASKiwi
PROC Star

@Quentin - It would also be helpful to actually run the generated code. If this was done then the first thing you notice is that a non-existent dataset called CONTROL_VALIDATED is referenced.

webart999ARM
Quartz | Level 8

@Quentin Thank you for your feedback. I combined a few references—including an LLM—to explore potential solutions and added my own logic. I understand the concern about indicating when code is machine-generated, and I’ll keep that in mind in future posts. Appreciate your input.

webart999ARM
Quartz | Level 8

Hi @Tom ,

Thanks for reviewing my previous suggestion in detail. You’ve raised some valid points, so I’d like to address them and clarify the limitations of the approach I described:

  1. TEMPFILENAME() Function

    • You’re correct—there’s no built-in TEMPFILENAME() function in SAS. The snippet I provided is a hypothetical or shorthand reference. In practice, one would use something like filename mytemp temp; to create a temporary file reference or manually define the file path for capturing output.
  2. Autocall Macros and Compilation

    • The %sysmacexist() function (and the %copy statement) indeed only work on macros that have already been compiled and stored in a SAS catalog with the /SOURCE option. If you rely on autocall libraries (where macros are compiled on-the-fly), this method isn’t directly helpful.
    • For SAS-supplied autocall macros (e.g., %LEFT, %UPCASE, etc.), there’s no straightforward way to pull the “source” from a compiled form. One would have to manually inspect the autocall files located in the SASAUTOS path.
  3. Stored Macros and Alternative Approaches

    • If all macros are compiled and stored with /STORE SOURCE, you could parse the underlying catalogs or simply “inventory” the macro definitions in one central dataset. This approach becomes a data-processing task: scanning the stored code to build a parameter list once and reusing that information.
    • If the macros are purely autocall, scanning the physical .sas files in your SASAUTOS directories is often the most practical route. You would look for lines starting with %macro and parse out the argument lists. Of course, you have to handle nuances like multiple macros per file or conditional macro definitions.

Ultimately, SAS does not provide a built-in, foolproof way to retrieve macro signatures dynamically—especially for autocall macros that aren’t yet compiled or don’t store source code in a catalog. The idea behind my suggested code was to demonstrate one possible way to parse macro parameters if you do have compiled macros with source available. I recognize it won’t fit every scenario, particularly when using autocall libraries or SAS-supplied macros.

If you must handle macros from multiple sources (autocall directories, compiled catalogs, SAS-supplied macros), a more robust approach would be:

  1. Maintain a Registry: Create and update a small “registry” dataset or table that lists each macro’s expected parameters. Whenever you define or modify a macro, you update this registry. Then, you can compare your intended call against the registry to prevent mismatched calls.
  2. File Scanning: For autocall macros, scan the .sas files in the autocall path to extract %macro%mend blocks. This is more manual but can be automated. The text-based method is essentially building your own indexing tool for macros.

Thank you for pointing out the limitations of my proposed approach. Your detailed feedback really helps pinpoint exactly where it won’t work, and it can guide others who consider similar solutions. It was nice to be involved in this discussion, experimenting with code to find new possibilities as this is something I have been also interested in recently.

Thanks again for your time and insights, and take care!

 

 

Tom
Super User Tom
Super User

If you already have the macros compiled then why not use the CATALOG engine to read the compiled macro and see if you can figure out where the parameter names are stored?

 

This seems to work on stand alone SAS.

%macro one(a1,b1); %mend;
%macro two(a2,b2,c2); %mend;
%let x=%left( abc );
data try ;
  set sashelp.vcatalg;
  where libname='WORK' and objtype='MACRO';
  length position 8 parameter $32 fname $100;
  fname=catx('.',libname,memname,objname,objtype);
  infile sasmacr catalog filevar=fname truncover length=ll firstobs=3 end=eof;
  do while(not eof);
    input ;
    if ll=48 then do;
      length position 8 parameter $32 ;
      position=sum(position,1);
      parameter=scan(substr(_infile_,9,32),1,'00'x);
      output;
    end;
    if string='04000000'x then leave;
  end;
  keep objname position parameter ;
  rename objname=macroname;
  label objname=' ';
run;
proc print;
run;

Results:

Obs    macroname    position    parameter

 1      LEFT            1        TEXT
 2      ONE             1        A1
 3      ONE             2        B1
 4      TWO             1        A2
 5      TWO             2        B2
 6      TWO             3        C2
 7      VERIFY          1        TEXT
 8      VERIFY          2        TARGET

You can run this code to check that the definition of %VERIFY() macro does have two parameters named TEXT and TARGET.

data _null_;
  infile sasautos('verify.sas');
  input;
  put _infile_;
run;

If you run it on SAS ODA you get a lot more compiled macros that SAS/Studio has created.  And also discover that perhaps it needs more work since it seems to find extra parameters for the VERIFY macro.

 

Tom
Super User Tom
Super User

Looks like the records with parameter names start with '02'x.  The other two rows for verify with the %GOTO label names.

Plus we needed a way to include the macros without parameters.

 

So try something like this:

data try ;
  set sashelp.vcatalg;
  where libname='WORK' and objtype='MACRO';
  length position 8 parameter $32 fname $100;
  fname=catx('.',libname,memname,objname,objtype);
  infile sasmacr catalog filevar=fname truncover length=ll firstobs=1 end=eof;
  position=0;
  do while(not eof);
    input ;
    if ll=48 and _infile_=: '02'x then do;
      length position 8 parameter $32 ;
      position+1;
      parameter=scan(substr(_infile_,9,32),1,'00'x);
      output;
    end;
    if _infile_='04000000'x then leave;
  end;
  if position=0 then output;
  keep objname position parameter ;
  rename objname=macroname;
  label objname=' ';
run;
Tom
Super User Tom
Super User

With a little more experimentation this seems to work very well.

data stored_macros ;
  length macname $32 order 8 parmname $32 call_by_position 8 default $200 ;
  set sashelp.vcatalg;
  where libname='WORK' and objtype='MACRO';
  macname=objname;
  fname=catx('.',libname,memname,objname,objtype);
  infile sasmacr catalog filevar=fname truncover length=ll dsd dlm='00'x end=eof;
  order=0;
  do while(not eof);
    input @;
    if _infile_=: '02'x then do;
      order+1;
      input @5 call_by_position pib1. @9 parmname @45 default @;
      output;
    end;
    input;
    if _infile_='04000000'x then leave;
  end;
  if order=0 then output;
run;

So I I define the macro %MYMAC to have two parameters that can be passed by POSITION and two that required the values to passed by NAME like this:

%macro mymac(in,out,named=,named2=default value); %mend;

And run the data step above I get these observations:

Tom_0-1737766910000.png

 

 

RichardAD
Quartz | Level 8

Tom went from No to an in-depth solution in a rather short time frame.

 

Yes, the original question was for compiled macros.  If the user was being reliant on autocall macros I would just let log show ERRORs if not correct.

 

Thanks Tom.

Tom
Super User Tom
Super User

If you are really using stored compiled macros you probably need to make sure to reference the right LIBNAME/MEMNAME combinations from SASHELP.VCATALG.

 

Also not hard to add extra code to compile the autocall macro first when it does not exist.

Something like:

%if not %sysmacexist(mymac) %then %do;
  %include sasautps(mymac);
%end;

If your SASAUTOS search path is more complicated then the logic is only a little bit more complicatd.  You can check this macro for an example of how to search the paths referenced by the SASAUTOS system option.

https://github.com/sasutils/macros/blob/master/maclist.sas

 

Patrick
Opal | Level 21

@webart999ARM 

Your answer here and for other questions feels very much like AI generated without actual validation.

Congrats to using AI but at current point in time AI really only can assist you in writing SAS code. The generated SAS code still needs validation and very often amendments to become useful.

hackathon24-white-horiz.png

The 2025 SAS Hackathon Kicks Off on June 11!

Watch the live Hackathon Kickoff to get all the essential information about the SAS Hackathon—including how to join, how to participate, and expert tips for success.

YouTube LinkedIn

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
  • 17 replies
  • 2566 views
  • 14 likes
  • 6 in conversation