Determine the folder of a SAS source file - sas

When I open a SAS file in enterprise guide and run it, it is executed on the server. The source file itself is located either on the production site or the development site. In both cases, it is executed the same server however. I want to be able to tell my script to store results in a relative folder. But if I write something like
libname lib_out xport "..\tmp\foobar.xpt";
I get an error, because the working folder of the SAS Enterprise Guide process is not the location of my source file, but a folder on the server. And the folder ..\tmp does not exist there. Even if it would, the server process does not have write permission in that folder.
I would like to determine from which folder the .sas file was loaded and set the working folder accordingly. In one case it's S:\Development\myproject\sas\foobar.sas and in the other case it's S:\Production\myproject\sas\foobar.sas
It this possible at all? Or how would you do this?

Depending on the way EG is configured, you may be able to use something like the syshostname global macro variable to determine where to save your results:
%macro sasdir;
%global sasdir;
%if "&syshostname" eq "mydevelopmenthost" %then %do;
%let sasdir = S:\Development;
%end;
%else %if "&syshostname" eq "myproductionhost" %then %do;
%let sasdir = S:\Production;
%end;
%mend;
%sasdir;
libname lib_out xport "&sasdir\myproject\sas\tmp\foobar.xpt";
If not, try looking at what other global or automatic macro variables may be able to help you by doing a:
%put _all_;
Hope this helps
Cheers
Rob

OK, this isn't going to answer your question exactly, but I have this macro easily available so I thought I would share it. From here you would just need to do a little string processing.
%macro progName;
%* Returns the name of current program;
%let progPath = %sysfunc(GetOption(SysIn));
%* if running in interactive mode, the above line will not work, and the next line should;
%if %length(&progPath) = 0 %then %let progPath = %sysget(SAS_ExecFilePath);
%str(&progPath)
%mend progName;

Related

How to tell SAS to compile a Macro each time it is called instead of doing it manually

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
;

SYSERR Automatic Macro Variable

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*/

Ending a SAS-Stored process properly

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.

SAS Conditional Libname Assignments and Error Log

I am using SAS Enterprise Guide 6.1 and am utilizing the "Submit SAS Code when server is connected" capability under Tools, SAS Programs to submit all of my personal credentials, libnames. Yes, I know there is probably an easier way to do this with SAS Management Console, but that is not an option right now.
I have several Teradata libraries I need to assign within each project, but the problem is that sometimes I (or more commonly somebody else on my team) will change my password and forget to change it in the start-up code. This results in several incorrect attempts and locks me out immediately. I wish to do a few things:
Create a conditional libname assignment that will execute all the libnames if the credentials are correct.
If the credentials are incorrect, the libnames are not executed (so that I don't lock myself out).
Since the SAS code in the "Submit SAS Code when server is connected" section doesn't seem to generate a log, I wish to send myself an email with the SAS Log attached (only if the credentials fail).
Kill the Server connection (if credentials error out) to avoid further attempts to assign libraries.
Here is my attempt, I'm having trouble attaching the log and setting up the email statement.
*Define personal credentials;
%let myemail=gollum#middleearth.com;
filename temp email "&myemail";
*Define Teradata credentials;
%let tera_user=Gollum; /*Teradata Username*/
%let tera_pwd=#filthy_hobbitses; /*Teradata Password*/
*Conditionally assign libraries;
%macro libsetup();
libname library1 teradata user=&tera_user password="&tera_pwd" tdpid=terap schema=library1 fastload=yes bulkload=yes fastexport=yes;
%if &syslibrc=0 %then
%do;
libname library2 teradata user=&tera_user password="&tera_pwd" tdpid=terap schema=library2 fastload=yes bulkload=yes fastexport=yes;
libname library3 teradata user=&tera_user password="&tera_pwd" tdpid=terap schema=library3 fastload=yes bulkload=yes fastexport=yes;
*more library statements here;
%end;
%else
%do;
data _null_;
file temp
subject="TERADATA CREDENTIALS ERROR"
attach=("put SAS LOG filename here");
put 'Teradata Login Failed. SAS LOG Attached.';
%abort abend;
%end;
%mend libsetup;
%libsetup;
Thanks.
Can't tell if your looking for suggestions on alternative approaches, or for help with the emailing.
For emailing, something like below (untested) should work:
filename __mymail email
to="gollum#middleearth.com"
from="gollum#middleearth.com"
subject="credential error"
attach="/home/mylog.log"
;
data _null_;
file __mymail;
put "Hi!";
run;
Note the server where SAS is executing has to have access to a mail server, and actual code needed may vary with mail protocol, etc.
For attaching the log, will probably need to use PROC PRINTTO to write your log to a file, and then use PROC PRINTTO again to let go of the file before you email it.
HTH

Update Library in SAS

I may or may not have a library named qa.my_library
I have a temp library (same columns, different data) in work.my_library_temp
My goal is to achieve this pseudo code
if qa.my_library doesn't exists
then qa.my_library = work.my_library_temp;
else qa.my_library = SQL UNION (qa.my_library, work.my_library_temp)
How would you write the code for it ?
To determine if a LIBRARY exists use the LIBREF function. To determine if a DATA SET exists use the EXIST function.
Here is some sample code:
%Global LIBEXISTS DSNEXISTS;
Options source source2 notes symbolgen mlogic mprint ;
%Macro Check(Lib=,DSN=);
%Let LIBEXISTS=0;
/* Outside the DATA STEP, use %SYSFUNC */
%IF %SYSFUNC(LIBREF(&LIB)) = 0 %THEN %Let LIBEXISTS=1; ;
%Let DSNEXISTS=0;
%IF %SYSFUNC(EXIST(&DSN)) > 0 %THEN %Let DSNEXISTS=1; ;
%Mend;
libname test 'c:\';
Data Test.Temp;
Test="Test";
Run;
%Check(Lib=test,dsn=test.temp) ;
%Put LIB EXISTS? &libexists;
%Put DSN EXISTS? &dsnexists;
libname test clear;
%Check(Lib=test,dsn=test.temp) ;
%Put LIB EXISTS? &libexists;
%Put DSN EXISTS? &dsnexists;
One of the reasons for the concept of libraries is to avoid name collisions. What you ask would be unable to deal with such collision (i.e., a dataset with the same name in both libraries). If you can be certain that there will be no collision, than there is actually no reason for what you want: there is no fundamental difference between remembering (and parsing) a unique dataset name or a unique dataset name with its library.
Leaves only the situation where there can be collisions, but you have one library who has priority then. You can write something that gets somewhat close to what you want:
Using SASHELP library, identify every dataset in library A and B. Assuming that A takes priority: for every dataset in B which is not in A, run the following:
data A.dataset_from_B /view=A.dataset_from_B;
set B.dataset_from_B;
run;
Again, it is not exactly the same, but probably serves your needs. But I would be really interested in knowing why you want this though.