To create a complex analysis, I have a program that will %include some different other programs, each one with a specific purpose (mainly one for each analysis, one for macros, one for formats and so on..). All the analysis run smoothly with one click on the final program.
Basically what I want now is to implement a system that will be able to rewrite all the code in one document: exploding the macros with the correct code, exploding the %do loops with the correct datastep code, deleting the %let statements but resolving the macro variables, and so on (example at the bottom of the post).
The only idea I had was to save the log, with the MPRINT option to write everything, delete the notes after the datastep executions and other things not datastep related. But this method is really dirty, therefore I would kindly ask if you have better ideas on how to proceed.
This is an example of 2 nested programs:
main.sas
-----------------------------------------------------
/* My MAIN PROGRAM */
%let PGM=MAIN;
%put This is my &pgm. program;
%let LIB=MYLIB;
%include &LIB.(FIRST.sas);
-----------------------------------------------------
first.sas
-----------------------------------------------------
%let ind=2;
%macro ABC;
%do rk=1 %to &ind.;
data A&rk.;
MYVAR=&rk.; output;
run;
%end;
%mend; %ABC;
-----------------------------------------------------
The document that I would get is something like:
/* My MAIN PROGRAM */
%put This is my MAIN program;
data A1;
MYVAR=1; output;
run;
data A2;
MYVAR=2; output;
run;
Any suggestion? Did you ever find out this? How you solved it? Many thanks.
See Example 2 here:
http://support.sas.com/documentation/cdl/en/mcrolref/69726/HTML/default/viewer.htm#p1dhqw0i5yj2m8n15opapnwteqra.htm
You can add these options to your program:
options mfile mprint;
filename mprint 'debugmac';
And all the generated code will appear in the specified output file. This will not include %PUT statements though - in your example, your desired output includes a %PUT statement. The options above just redirect all the normal MPRINT lines to a file, so no open code or macro statements are include. You could probably find a way to append a header to the MPRINT file though, depending on your exact needs.
Having said all that, I wouldn't recommend your approach. Why would you want to flatten the program in this way? It's extremely unlikely to have any noticeable performance benefit, and will make it much harder to debug or modify the program in future.
Related
As part of my work, I am going through this SAS code. I have never worked on SAS before. Could anybody explain the usage of '%' in front of the SAS lines in the code below?
%if &i>0 %then %do;
or
%put ##### calling formula;
%formula();
% signs indicate macros or macro code. Macros in SAS are similar to functions in other programming languages, but not quite. You can treat them like functions though. They deal exclusively with text and only text.
The macro facility exists to handle more generalized problems. Things in SAS are done in PROCs and the DATA Step. Each PROC and DATA Step is like its own little self-contained environment - stuff that happens in there stays in there. The Macro facility gives you tools to do things like conditionally call PROCs, the DATA Step, or system options.
I highly recommend taking one of the free training courses on SAS programming. If you want to get a jump start on Macro language, start with this free course on Coursera.
To help get you started, here's what this code is doing, line-by-line.
%if &i>0 %then %do;
If the macro variable, i, is greater than 1, run some code. All macro variables start with &. The value of the macro variable i can be reviewed by typing %put &i.
%put ##### calling formula;
%put writes a line to the log in open code. This is likely being used to help debug things.
%formula();
This is a macro function that holds some code and runs it. It has no arguments. Macro functions are created with the following syntax:
%macro myMacro();
<macro or SAS code here>;
%mend;
% is the key symbol to invoke a macro. If we wanted to invoke myMacro, we can do so by prefixing it with a %:
%myMacro;
Context: We use SAS 9.4 and Enterprise Guide 7.15. Currently, we are implementing some new Macros and of course have to change a lot along the way. Sometimes smaller, sometimes bigger changes. The problem is, that in order to get the changes to work, SAS needs us to manually compile the Macro code or to restart the session which is a bit tedious.
This is the current setup in our main file (which calls all the Macros):
/* Macro options */
MAUTOSOURCE sasautos = "<path to macro>" mlogic mlogicnest mprint mprintnest MRECALL
Is it possible, while using the MAUTOSOURCE */ sasautos ="" option, to tell SAS every time the Macro is called to actually also compile the Macro instead of using the session-stored Macro? Ideally, the Macro would only be compiled when the whole line of code from the main file (MAUTOSOURCE */ sasautos ="" etc.) is executed, otherwise it should keep a compiled version in the session.
I found this paper (The Autocall Macro Facility in the SAS for Windows Environment) which states in the conclusion
After that SAS will use the code that has already
been compiled. If changes are made to the macro, it must be
compiled again before the changes go into effect.
which I hope doesn't mean that I have to do it manually. Is there any Macro option to set?
In SAS 9.3 they added the %SYSMACDELETE macro function. So if you just want to let autocall redefine a single macro then use that to remove the current definition.
%symacdelete mymacro;
Here is a utility macro that will query SASHELP.VCATALG view to find compiled macros in the WORK library and delete them. It has options to either list names of macros to delete or keep. Note that normal SAS sessions use WORK.SASMACR to store the compiled macros. But SAS/Studio and EG (and perhaps other ways of running SAS) use WORK.SASMAC1 instead.
https://github.com/sasutils/macros/blob/master/macdelete.sas
%macro macdelete(delete,keep);
/*----------------------------------------------------------------------------
Remove compiled macros using %SYSMACDELETE macro statement.
Use DELETE parameter to list macro names to delete.
Use KEEP parameter to list macro names to NOT delete.
Calling it with no values will delete all macros not currently running.
----------------------------------------------------------------------------*/
%local libname memname objname objtype fid i;
%do i=1 %to %sysmexecdepth;
%let keep=%sysmexecname(&i) &keep;
%end;
%if %length(&delete) %then %let delete=and findw("&delete",objname,',','sit');
%let fid=%sysfunc(open( sashelp.vcatalg(keep=libname memname objname objtype
where=(libname='WORK' and objtype='MACRO' and memname like 'SASMAC_'
and not findw("&keep",objname,',','sit') &delete))));
%if (&fid) %then %do;
%syscall set(fid);
%do %while(0=%sysfunc(fetch(&fid)));
%put %sysfunc(compbl(Removing &objname from &libname catalog &memname));
%sysmacdelete &objname;
%end;
%let fid=%sysfunc(close(&fid));
%end;
%else %put %qsysfunc(sysmsg());
%mend macdelete;
Example:
3348 %macro test1; %mend;
3349 %macro test2; %mend;
3350 %macro test3; %mend;
3351 %macro test4; %mend;
3352 %macdelete(test1 test3);
Removing TEST1 from WORK catalog SASMACR
Removing TEST3 from WORK catalog SASMACR
3353 %macdelete(keep=test2);
Removing TEST4 from WORK catalog SASMACR
Example when running SAS/Studio or Enterprise Guide:
97 %macro test1; %mend;
98 %macro test2; %mend;
99 %macro test3; %mend;
100 %macro test4; %mend;
101 %macdelete(test1 test3);
Removing TEST1 from WORK catalog SASMAC1
Removing TEST3 from WORK catalog SASMAC1
If you delete the compiled macro from WORK.SASMACR SAS will have to re-compile the macro when you call it again.
proc catalog c=work.sasmacr;
*contents;
delete your-macro-to-recompile.macro;
run;
quit;
For your normal users you should set a release schedule of changes. The users will know they need to re-start a new session after the release is made.
For your developers that are testing the changes as they are made they just need to use %INCLUDE to re-compile the macro. So if you know that macro XYZ is changed then just run:
%include maclib('xyz.sas');
Or you could brute force it and recompile all of the macros in your autocall library.
%incldue maclib('*.sas');
You could get fancier and make a macro that cleans out the actual catalog of compiled macros. Something like:
%macro clean_autocall;
proc catalog force c=work.sasmacr;
save clean_autocall /et=macro;
quit;
options mrecall mautosource;
%mend clean_autocall;
But if you are using Enterprise Guide there are two issues.
First for some reason it uses a different catalog to store the compiled macros. (Why?) I think it is WORK.SASMAC1 instead of WORK.SASMACR.
Second EG will manually compile a bunch of helper macros that it needs. I am not sure if there is an official source for the complete list of these macros? You could try adding code to your project to automatically create the list based on what entries are in the catalog when your project starts. Here is a list I made 10+ years ago when I tried using EG with production environment. But I am sure that it is out of date.
%let s_eg_save= checkfmt checkhotfix
eclibassign eclibunassign enterpriseguide gaccessible
_eg_conditional_dropds
;
I've been struggling with this problem for a while now. I have written a macro lets call it macro1 and I want to run it on a list of years lets say 1989 1990 1995 using a do or %do loop in open code but I cannot get it to work. I have been trying things along the lines of
%let years = 1989 1990 1995
%local i next_year
%do i=1 %to %sysfunc(%countw(&years));
next_year = %scan(&years, &i);
%macro1(&next_year);
%end
to no avail. Does anyone have a convenient solution to this problem? It seems like something that another programming language would be able to do in 3 lines (I have to use SAS though as its the only available language). Any help would be great.
Your code could work inside a macro definition if you include the required %LET needed to create a macro variable. And terminate your statements with the required semi-colons. And also leave off the semi-colons you don't need.
%macro test(years);
%local i next_year;
%do i=1 %to %sysfunc(%countw(&years));
%let next_year = %scan(&years, &i);
%macro1(&next_year)
%end;
%mend test;
%test(1989 1990 1995)
You could also eliminate the unneeded NEXT_YEAR variable completely.
%macro1(%scan(&years, &i))
Really you should just avoid using macro coding. Stick to learning how to write actual SAS code first. Wait to learn how to use macros to generate code until you know what code you want to generate. Complex DO loops are much easier to create using the real language of a data step instead of trying to code in a macro processor.
data _null_;
do year=1989, 1990, 1995 ;
call execute(cats('%nrstr(%macro1)(',year,')'));
end;
run;
But sometimes wallpaper code is easier than over complicating things. Here is the three line program.
%macro1(1989)
%macro1(1990)
%macro1(1995)
I have several programs in one SAS project, i.e program A -> program B ->.... I want to build dependency between programs using macro variables.
Program A will process few data step and procs. If ANY procedure in program A executes with errors, I would like to run program C. Otherwise run program B.
This seems to be tricky since syserr resets at each step boundary. If first data step in program A executes with error and the rest don't, then at the end of program A, syserr is still 0. I need macro variable value to be something other than 0 once error happens and the value can keep till program ends.
If the program dependency is based on other criteria (say values), the user-defined macro variables can handle that. For something related to system errors, I think SAS already has something can do the trick.But I can't find anything else except syserr, which doesn't seem to help.
Note: I find this SAS stop on first error. But basically it's to check the error status after each data step. That sounds crazy if program A contains 50+ data steps.
Easy - just use syscc!
SYSCC is a read/write automatic macro variable that enables you to
reset the job condition code and to recover from conditions that
prevent subsequent steps from running.
See documentation, but I guess you will be looking for something like:
%if &syscc > 4 %then %do;
%inc "/mypath/pgmB.sas";
%end;
%else %do;
%inc "/mypath/pgmA.sas";
%end;
The highest value of syscc is retained across step boundaries, always with an integer to represent the error level. Example values:
The values for SYSCC are:
0 is no errors no warnings
4 is warnings
greater than 4 means an error occurred
Note that there are some things it won't catch, but to improve it's effectiveness you can use:
options errorcheck=strict;
Finally - you mention 'sas project', if by this you mean you are using Enterprise Guide then please be aware of the advice in this usage note.
You could define a macro that kept track of the error status and run this after each step. The macro would look something like this:
%macro track_err;
%global err_status;
%let err_status = %sysfunc(max(&err_status, &syserr ne 0));
%mend;
Usage example below. First initialize the value to track the overall error status. The first datastep will fail, the second will run successfully and the final value of err_status will be 1.
%let err_status = 0;
data oops;
set sashelp.doesnt_exist;
run;
%track_err;
data yay;
set sashelp.class;
run;
%track_err;
%put &=err_status;
Final output:
ERR_STATUS=1
As far as 'sounding crazy to check the status after each step'... well SAS doesn't provide something for the exact requirement you have, so the only way to do it is literally to have something check after each step.
EDIT: Correction - looks like the syscc approach mentioned in RawFocus's answer actually shows there IS something that does this in SAS.
If you want the check to 'blend in' more with the code, then consider replacing your run and quit statements with a macro that performs the run/quit, then checks the status all in one. It will result in slightly cleaner code. Something like this:
%macro run_quit_track;
run;quit;
%global err_status;
%let err_status = %sysfunc(max(&err_status, &syserr ne 0));
%mend;
%let err_status = 0;
data oops;
set sashelp.doesnt_exist;
%run_quit_track;
data yay;
set sashelp.class;
%run_quit_track;
%put &=err_status;
Use one macro, call it runquitA. call this macro at the end of each proc sql or proc data in the place of quit; and run;
Example:
/*Program A*/
%macro runquitA;
; run; quit;
%if &syserr. ne 0 %then %do;
/*Call Program C*/
%end;
%mend runquitA;
proc sql;
create table class1 as
select * from sashelp.class;
%runquitA;
data class2;
set sashelp.class;
%runquitA;
/*Call Program B*/
/*end of Program A*/
I have a SAS-Storedprocess which does some html-output via webout.
Under some circumstances (e.G.: no data available) i want to put a custom error message and stop the process from further execution.
I have a makro-solution which had worked fine so far:
%if &varnobs = 0 %then %do;
data _null_;
file _webout;
put " &text1";
put " &text2";
put " &text3";
run;
ENDSAS;
%end;
But now i was informed that the use of ENDSAS is not allowed within our company because it can have various side-effects and also can stop not only the process, but also the complete session.
Now i am looking for alternatives, i had also tried the abort statement with several options, but one problem there was, that abort puts an error message in the log, so that not my custom message is shown, but a sas error message. Also in the abort documentation is stated (abort), that abort not only stops the process, but also the session.
I know there are programatically alternatives like if-else or goto instead of stopping the process, but that is not an option for my specific problem.
So the question is:
How to stop a stored process during execution, without stopping the session, without other side effects and without an SAS error-message?
Indeed, endsas; can cause unrecoverable issues in some SAS environments, particularly 9.4m3
For that reason in the SASjs Core library we had to resort to an 'out of the box' method for preventing further execution
We call it "skippy" - simply open a macro (or two) and don't close them!
filename skip temp;
data _null_;
file skip;
put '%macro skip(); %macro skippy();';
run;
%inc skip;
This is the full macro we use in SASjs: https://github.com/sasjs/core/blob/main/base/mp_abort.sas
Have you tried turning setting NOERRORABEND? Might be a possible option.
ERRORABEND / NOERRORABBEND
Turn off ERRORABEND for an optional, potentially unstable part of a
SAS
program
This is an interesting, tricky area. In some settings, setting system options:
options obs=0 noreplace;
might work as proxy.
you can do if else approach to prevent extra code from running and print a custom message error as
%if &data ne "" %then;
*your code;
%else
data _null_;
file _webout;
put 'html code of custom message';
run;
SAS is horrible at error handling so my suggestion is to avoid errors whenever possible ;-)
Jokes aside, yeah, there's limited options aside from endsas and %abort cancel;
You could try moving all code after the check into an %include statement that gets conditionally executed. Something like:
%macro conditional_execute;
%if &some_condition %then %do;
%include "rest_of_logic_to_perform.sas";
%end;
%else %do;
%put ERROR: CUSTOM ERROR MESSAGE HERE;
%end;
%mend;
The reason for an include rather than putting all the code inside of the macro is that I'm assuming there's a substantial amount of processing still to be performed. If it is a short amount of code then yes you could just put it within the %if condition.