Get case sensitive list of macros in program - sas

Based on this post, I can get a list of compiled macros in a program, ordered by their first appearance, using the following code:
%macro FirstMacro();
%put Hello, World!;
%mend;
%macro SecondMacro();
%put Goodbye, cruel world!;
%mend;
proc sql;
select objname, created
from dictionary.catalogs
where objtype='MACRO' and libname ^= 'SASHELP'
order by created
;
quit;
This, however, gives all the macros in uppercase.
Object Name Date Created
--------------------------------------------------
FIRSTMACRO 09FEB17:16:12:31
SECONDMACRO 09FEB17:16:12:31
I use PascalCase for my macro names (as illustrated above). Is there a way to get a similar list, but with case sensitivity preserved?
It seems like PROC SCAPROC might provide such a list, but it's unclear to me. There is a note from when PROC SCAPROC was first released (in 9.2) that indicates that PROC SCAPROC doesn't provide support for when a %MACRO is used. However, the note says this will be a feature in future versions of SAS. With SAS 9.4 it's unclear whether the feature has been added yet.
The only other alternative I can think of is to write a script in some other language which analyzes the text above for the macro names.

So, it's feasible this may give you what you want, at least with the above example.
It does not expand the macro when it's run - meaning, you don't get to see what happens inside the macro, as it runs. This may be a problem if you are generating macros at runtime, i.e. using your data to create macros - like
data _null_;
set something;
call execute ('%macro ',name_var,'; ... ; %mend;');
run;
That may not show up properly. But if you're just writing macros in code and compiling them, then yes, you might be able to use this - though you have to run the compilation step at least as part of this (this doesn't analyse plaintext, it analyzes a job while it runs).
So if I do something like this:
proc scaproc;
record 'c:\temp\recording.txt' expandmacros;
run;
%macro PascalCaseMacro();
%put PascalCaseMacro;
%mend;
%PascalCaseMacro;
proc scaproc;
write;
run;
I get output like this:
/* JOBSPLIT: PROCNAME DATASTEP */
/* A bunch of lines like that with JOBSPLIT at the start */
%macro PascalCaseMacro();
%put PascalCaseMacro;
%mend;
%PascalCaseMacro;
/* Some more JOBSPLIT lines */
You could then parse that text file for lines that started with %macro.
I do think the technical answer to your question is "no", they have not implemented this: while it does update the technical information with some accesses of the macro catalog, it doesn't ever include in the detail of what macro was run (in the /* JOBSPLIT */ lines). That's really what that technical note is talking about, I think; i.e., this is not perfectly useful for optimizing macro code. The actual code that generates the macro (which I think is all you need) is there. (However, if you have a stored-compiled-macro, that would not be available.)

I was able to adapt the Windows Batch Script from this post to get a list of case sensitive macros defined in a given program.
REM GetListOfDefinedMacros.bat
#ECHO OFF
SET /P SASFILE= Enter SAS filepath:
SET SASFILE=%SASFILE:"=%
ECHO.
ECHO The defined macros are:
ECHO.
FOR /F "tokens=*" %%A IN ('FINDSTR "macro" "%SASFILE%" ^| FINDSTR "("') DO CALL :FindString "%%A"
ECHO.
pause
GOTO :eof
:FindString
SET String=%~1
SET String=%String:*macro =%
SET String=%String:)=X%
SET String=%String:(=`%
FOR /F "tokens=1 delims=`" %%A IN ('ECHO.%String%') DO ECHO.%%A
GOTO :eof
The program prompts the user for the SAS program's full file path. It then parses the code, looking for lines which contain both "macro" and "(", returning the any characters that lie between them.
The following assumptions must hold:
each line contains a single programming statement; "macro" and "(" must be on the same line,
a macro is defined using parentheses; it will not work for %macro MacroName; style definitions.
Note that the script doesn't list compiled macros. It can only detect macros which have definitions within the given program. Macros compiled through %include or AUTOCALL will not show up.
For example,
/*C:\temp\sample sas program.sas*/
%macro FirstMacro();
%put NOTE: Hello, world!;
%mend;
%macro SecondMacro();
%put ERROR: Goodbye, cruel world!;
%mend;
%macro ThirdMacro(input);
%if &input. = hi %then %FirstMacro();
%else %SecondMacro();
%mend;
%ThirdMacro(hi);
%ThirdMacro(bye);
This gives the following output to the command prompt:
Enter SAS filepath: "C:\temp\sample sas program.sas"
The defined macros are:
FirstMacro
SecondMacro
ThirdMacro
Press any key to continue . . .
From there, you can copy and paste the list. Or, the code could be modified to redirect the list to a file.

Here is a native SAS solution which should rely only on BASE capabilities. It is intended for use in the Enhanced Editor on Windows.
%macro ListMacroDefinitions();
/*Get file-path of current program's source code.*/
proc sql noprint;
select distinct xpath
into : _SASProgramPath trimmed
from dictionary.extfiles
where xpath contains "%sysget(SAS_EXECFILENAME)"
;
quit;
/*Read in source code, parse for macro names, and
output to log.*/
options nonotes;
data _null_;
infile "&_SASProgramPath." dlm = '```';
length
line $ 256
macro_name $ 32
;
input line;
if line =: '%macro' then do;
indexLeftParenthesis = index(line, '(');
macroNameLength = indexLeftParenthesis - 8;
macro_name = substr(line, 8, macroNameLength);
put macro_name;
end;
run;
options notes;
dm 'wpgm';
%mend;
It works by first determining what the current program's name is using %sysget(SAS_EXECFILENAME). The %SYSGET function returns the value of the environment variable SAS_EXECFILENAME which
specifies the filename of the current SAS source file, if it is
submitted from the enhanced editor.
It then looks in the DICTIONARY.EXTFILES table and extracts a file path associated with name of the current source file (i.e. the program currently being edited). This file path is assigned to a macro variable.
Next, a data step reads in the source file of the current program. It reads the file line by line setting a non-existent delimiter (three back ticks) in the INFILE statement.
By default, if the INPUT statement tries to read past the end of the
current input data record, then it moves the input pointer to column 1
of the next record to read the remaining values.
Defining the delimiter to a string which never appears in the source code causes the line read in to never be parsed and thus be read in whole. Each line is checked for whether it contains a macro definition (via the =: operator). Specifically, it looks for the string '%macro'. We then manually parse each definition line for the characters between the '%macro ' statement and the first parenthesis. The result is output to the log. During this process, notes are disabled. Finally, the windowing command wpgm is issued. When the macro names are written to the log, the log window is selected. The wpgm command returns focus to the last program window, which should be the program currently being edited.
The whole process is put inside a macro. If you place the macro in an AUTOCALL library (found by issuing %put %sysfunc(pathname(sasautos));), you can get a list of all the macros defined within your current program. Note that for this list to be complete, you must save your current program before running the macro.

Related

I there a way to customize a shortcut to "show macrovariable value" in the SAS log

I work with a lot of different SAS-program, coded by a handful of different people. Sometimes its difficult to remember the values of a given macro-variable, I usually type;
rsubmit;
%put &Macroname. ;
endrsubmit;
(I have rsubmit; defined a shortcut)
But this - accumulated takes a substantial amount of time.
Is it possible to create a user defined shortcut, which, when a word is
selected, will write the macro-variable value in the log.
So is it possible to define a shortcut (ex. F6) that will execute;
rsubmit; %put &TTTT.;
of whatever selected word, TTTT ?
As Tom said in the comments above, to print any macro variable you can simply use %put &=mvar;.
Is it possible to create a user defined shortcut, which, when a word is selected, will write the macro-variable value in the log.
So is it possible to define a shortcut (ex. F6) that will execute?.
No, but you can define macro to generate code %put &=mvar; for your program using hotkeys and simlpy put the var name to be displayed instead of "mvar".(Program->Editor Macros->Macros->Create if u work with Guide)
More info here.
In DMS you can setup a function key definition by issuing the KEYS command.
A key definition issues one or more commands, and the command GSUBMIT will submit code for you!
So this definition should display the remote sessions user macro variables
Ctrl-T gsubmit 'rsubmit; %put _user_;'
Where as this definition will display the user macro variables of the current session.
Ctrl-U %put _user_
If you don't have a remote connection, try out this definition for gsubmit demonstration
Ctrl-W gsubmit 'proc print data=sashelp.class; run;'
Assuming you are using SAS Display Manager then just type the %PUT command on the command line.
Or use gsubmit command to submit non macro code.

Converting autocall libraries to stored compiled macros

We have hundreds of macros across several autocall libraries that I'd like to compile using the MSTORE facility.
The problem is that some of those libraries are used on other sites - and we don't want to add /STORE to every definition, as then it is required to use the MSTORED option.
I guess the answer is to add /* /STORE */ at the end of every %macro statement, so that the .sas program files can be updated with a regular expression (or some other text replacement technique) on every redeploy, but this seems like a messy workaround.
Is it possible / in some way supported, to compile regular (unmodified) autocall macros and store the definitions? Eg by %inc'ing each one and saving the work macro catalog, or something like that?
I won't say definitively that this isn't possible, but I can report that I tried to do the same thing some time ago and got stuck on the same point. I was also unable to find any way of doing this other than adding /store to every %macro statement.
I vaguely remember that I was able to upload the work.sasmacr catalogue from one session to another on the same machine (after first compiling a few autocall macros to populate it), but the other session didn't recognise the macro definitions from transferred catalogue even though the appropriate options were set for using stored compiled macros.
My motivation was different from yours - I was looking for a way to define a macro in one session and execute it in another without saving it in an autocall folder or %including it in both sessions - but the conclusion was the same.
What is the problem that you had?
First compile all of your autocall macros. Say you have a fileref named MYMACS that points to the directory with the source code.
%include mymacs(macro1,macro2,.....);
You might use a program to search for all of the source files so that you could automate generating the %include statement(s). Or you could use a datastep and copy all of the source files into a single temporary file and include that.
filename src temp;
data _null_;
infile "&inpath\*.sas" ;
file src ;
input;
put _infile_;
run;
%inc src ;
Then copy the WORK catalog to you a new location. Note that the name is different if you are running SAS on an application server. In that case try copying from WORK.SASMAC1 instead of WORK.SASMACR.
libname out base "&path";
proc catalog cat=work.sasmacr et=macro ;
copy out=out.sasmacr ;
run;
quit;
You can test if it worked by clearing your current work macro catalog, so you know SAS is not finding the macro there, and setting options to point to the new catalog of compiled macros.
proc catalog cat=work.sasmacr kill force ;
quit;
options mrecall mstored sasmstore=out ;
Then trying to run one of the copied compiled macros.
Now start up a new session and try using the compiled macros in that session.
Here was the approach I took for compilation (of course there are many alternative ways). The locations to query can be extracted from:
%put %sysfunc(getoption(sasautos));
The approach relies on macros being closed off with )/*/STORE SOURCE*/; as follows:
%macro some_macro(var1=x, var2=y
)/*/STORE SOURCE*/;
The SAS code has to be a program, as you can't create a stored compiled macro from within a macro.
/* set mstore options */
options mstored sasmstore=yourlib;
/* get list of macros */
/* taken from macrocore library */
/*https://github.com/sasjs/core/blob/main/base/mp_dirlist.sas*/
%mp_dirlist(path=/location/one,outds=in1)
%mp_dirlist(path=/location/two,outds=in2)
/* set up a temporary fileref */
filename tmp temp;
/**
* write each source macro to the fileref
* and switch on the STORE option
*/
data want;
set in1 in2;
infile dummy filevar=filepath end=last ;
file tmp;
do until(last);
file tmp;
input;
if _infile_=')/*/STORE SOURCE*/;' then put ')/STORE SOURCE;';
else put _infile_;
end;
run;
%inc tmp; /* compile macros */
filename tmp clear; /* clear ref */

Importing single value from a CSV file not working in SAS

I'm trying to use a Macro that retrieves a single value from a CSV file. I've written a MACRO that works perfectly fine if there is only 1 CSV file, but does not deliver the expected results when I have to run it against more than one file. If there is more than one file it returns the value of the last file in each iteration.
%macro reporting_import( full_file_route );
%PUT The Source file route is: &full_file_route;
%PUT ##############################################################;
PROC IMPORT datafile = "&full_file_route"
out = file_indicator_tmp
dbms = csv
replace;
datarow = 3;
RUN;
data file_indicator_tmp (KEEP= lbl);
set file_indicator_tmp;
if _N_ = 1;
lbl = "_410 - ACCOUNTS"n;
run;
proc sql noprint ;
select lbl
into :file_indicator
from file_indicator_tmp;
quit;
%PUT The Source Reporting period states: &file_indicator;
%PUT ##############################################################;
%mend;
This is where I execute the Macro. Each excel file's full route exists as a seperate record in a dataset called "HELPERS.RAW_WAITLIST".
data _NULL_;
set HELPERS.RAW_WAITLIST;
call execute('%reporting_import('||filename||')');
run;
In the one example I just ran, The one file contains 01-JUN-2015 and the other 02-JUN-2015. But what the code returns in the LOG file is:
The Source file route is: <route...>\FOO1.csv
##############################################################
The Source Reporting period states: Reporting Date:02-JUN-2015
##############################################################
The Source file route is: <route...>\FOO2.csv
##############################################################
The Source Reporting period states: Reporting Date:02-JUN-2015
##############################################################
Does anybody understand why this is happening? Or is there perhaps a better way to solve this?
UPDATE:
If I remove the code from the MACRO and run it manually for each input file, It works perfectly. So it must have something to do with the MACRO overwriting values.
CALL EXECUTE has tricky timing issues. When it invokes a macro, if that macro generates macro variables from data set variables, it's a good idea to wrap the macro call in %NRSTR(). That way call execute generates the macro call, but doesn't actually execute the macro. So try changing your call execute statement to:
call execute('%nrstr(%%)reporting_import('||filename||')');
I posted a much longer explanation here.
I'm not too clear on the connections between your files. But instead of importing the CSV files and then searching for your string, couldn't you use a pipe command to save the results of a grep search on your CSV files to a dataset and then read just in the results?
Update:
I tried replicating your issue locally and it works for me if I set file_indicator with a call symput as below instead of your into :file_indicator:
data file_indicator_tmp (KEEP= lbl);
set file_indicator_tmp;
if _N_ = 1;
lbl = "_410 - ACCOUNTS"n;
data _null_ ;
set file_indicator_tmp ;
if _n_=1 then call symput('file_indicator',lbl) ;
run;

Using a dynamic macro variable in a call symput statement

I posted a question a while back about trimming a macro variable down that I am using to download a CSV from Yahoo Finance that contains variable information on each pass to the site. The code that was suggested to me to achieve this was as follows:
data _null_;
a = "&testvar.";
call symputx('svar',trim(input(a,$8.)));
run;
That worked great, however I have since needed to redesign the code so that I am declaring multiple macro variables and submitting multiple ones at the same time.
To declare multiple macros at the same time I have used the following lines of code:
%let svar&e. = &svar.;
%put stock_ticker = &&svar&e.;
The varible &e. is an iterative variable that goes up by one everytime. This declares what looks to be an identical macro to the one called &svar. everytime they are put into the log, however the new dynamic macro is now throwing up the original warning message of:
WARNING: The quoted string currently being processed has become more than 262 characters long. You
may have unbalanced quotation marks.
That i was getting before i started using the symputx option suggested in my original problem.
The full code for this particular nested macro is listed below:
%macro symbol_var;
/*here the start row and end row created in the macro above are passed to this nested macro and then passed through the*/
/*source dataset. at the end of the loop each ticker macro variable is defined in turn for use in the following nested*/
/*macro, symbol by metric.*/
%do e = &beg_point. %to &end_point. %by 1;
%put stock row in dataset nasdaq ticker = &e.;
%global svar&e;
proc sql noprint;
select symbol
into :testvar
from nasdaq_ticker
where monotonic() = &e.;
quit;
/*convert value to string here*/
data _null_;
a = "&testvar.";
call symputx('svar',trim(input(a,$8.)));
run;
%let svar&e. = &svar.;
%put stock_ticker = &&svar&e.;
%end;
%mend;
%symbol_var;
Anyone have any suggestions how I could declare the macro &&svar&e. directly into the call synputx step? It currently throws up an error saying that the macro variable being created cannot contain any special characters. Ive tried using &QUOTE, %NRQUOTE and %NRBQUOTE but either I have used the function in an invalid context or I haven't got the syntax exactly right.
Thanks
Isn't this as simple as the following two line data step?
%macro symbol_var;
/*here the start row and end row created in the macro above are passed to this nested macro and then passed through the*/
/*source dataset. at the end of the loop each ticker macro variable is defined in turn for use in the following nested*/
/*macro, symbol by metric.*/
data _null_;
set nasdaq_ticker(firstobs=&beg_point. obs=&end_point.);
call symputx('svar' || strip(_n_), symbol);
run;
%mend;
%symbol_var;
Or the following (which includes debugging output)
%macro symbol_var;
/*here the start row and end row created in the macro above are passed to this nested macro and then passed through the*/
/*source dataset. at the end of the loop each ticker macro variable is defined in turn for use in the following nested*/
/*macro, symbol by metric.*/
data _null_;
set nasdaq_ticker(firstobs=&beg_point. obs=&end_point.);
length varname $ 32;
varname = 'svar' || strip(_n_);
call symputx(varname, symbol);
put varname '= ' symbol;
run;
%mend;
%symbol_var;
When manipulating macro variables and desiring bullet-proof code I often find myself reverting to using a data null step. The original post included the problem about a quoted string warning. This happens because the SAS macro parser does not hide the value of your macro variables from the syntax scanner. This means that your data (stored in macro vars) can create syntax errors in your program because SAS attempts to interpret it as code (shudder!). It really makes the hair on the back of my neck stand up to risk my program at the hands of what might be in the data. Using the data step and functions protects you from this completely. You will note that my code never uses an ampersand character other than the observation window points. This makes my code bullet proof regarding what dirty data there may be in the nasdaq_ticker data set.
Also, it is important to point out that both Dom and I wrote code that makes one pass over the nasdaq_ticker data set. Not to bash the original posted code, but looping in that way causes a proc sql invocation for every observation in the result set. This will create very poor performance for large result sets. I recommend developing an awareness of how many times a macro loop is going to cause you to read a data set. I have been bitten by this many times in my own code.
Try
call symputx("svar&e",trim(input(a,$8.)));
You need double quotes ("") to resolve the e macro.
As an aside, I am not sure you need the input statement if $testvar is a string and not a number.
I would have written this as
%macro whatever();
proc sql noprint;
select count(*)
into :n
from nasdaq_ticker;
select strip(symbol)
into :svar1 - :svar%left(&n)
from nasdaq_ticker;
quit;
%do i=1 %to &n;
%put stock_ticker = &&svar&i;
%end;
%mend;

Using SAS macro to import multiple txt files with sequential names

I have 4 txt files that need to be loaded to SAS and save them as 4 sas files. Here are how the text files look like: cle20130805.txt, cle20130812.txt, cle20130819.txt and cle20130826.txt . I used a % Do loop under % Macro in order to get the 4 files imported with only one invoke of the Macro. So Here is my code:
%macro cle;
%do i=20130805 %to 20130826 %by 7;
Data cleaug.cle&i;
infile "home/abc/cle&i..txt" dlm= '|' dsd firstobs=1 obs=100;
input a_no b_no c_no;
run;
%end;
%mend cle;
%cle
I am expect to have 4 sas file saved with only invoke the marco once. However it just can't run successfully. Any ideas where am I doing wrong in the code?
Thanks,
I don't recommend you try to write one macro to import all four files. Either it will be a specific macro you only ever use once - in which case you could just write this by hand and save the time you've already spent - or it will be something you have to modify every single month or whatever that you use it.
Instead, make the macro something that does precisely one file, but includes the information needed to call it easily. In this case, it sounds like you need one parameter: the date, so 20130805 or whatnot. Then give it a reasonable name that really says what it does.
%macro import_files(date=);
Data cleaug.cle&date.;
infile "home/abc/cle&date..txt" dlm= '|' dsd firstobs=1 obs=100;
input a_no b_no c_no;
run;
%mend import_files;
Now you call it:
%import_files(date=20130805)
%import_files(date=20130812)
%import_files(date=20130819)
%import_files(date=20130826)
Just as easy as the macro you wrote above, even hardcoding the four dates. If the dates are predictable in some fashion, you can generate the macro calls very easily as well (if there are more than 4, for example). You could do a directory listing of the location where the files are, or call the macro from a data step using CALL EXECUTE if you really like looping.