SAS: Preserve leading space in macro variable resolution - sas

I am using the SAS DDE functionality to call Excel commands. The command I'm using takes in the worksheet name. Unfortunately, the worksheet name I am working with contains a preceding space. Spaces can usually be handled with %str( ). In this case, the quoting gets tricky:
%let sheet = %str( )Sheet X;
filename cmds DDE 'excel|system';
data _null_;
file cmds;
put '[WORKBOOK.SELECT("&sheet.")]';
run;
This causes an error with the DDE session. The sheet name is not resolving as intended. I know this because the command works when the sheet is given explicitly:
put '[WORKBOOK.SELECT(" Sheet X")]';
How can I write the put statement such that %let sheet = %str( )Sheet X; resolves properly?

The issue isn't with the space, it's with the single quotes.
This works fine for me:
put "[WORKBOOK.SELECT(""&sheet."")]";

Related

SAS Proc Datasets - Change dataset name using macro variable

I want to change dataset names in SAS using concatenated macro variables. Using the example code below I get an error. Is my syntax wrong or is it not possible to use a concatenating function in this way?
Code:
%let term=201610;
%let emp='bob';
Proc Datasets library=work;
change testset = cat(&emp,&term);
run;
Errors Received:
ERROR 22-322: Syntax error, expecting one of the following: ALTER, MEMTYPE, MT, MTYPE, PROTECT, PW, READ, WRITE.
ERROR 76-322: Syntax error, statement will be ignored.
You don't need to concatenate macro variables, it's like a find and replace text.
Because you have quotes right now this is what would happen:
change testset = cat('bob', 201610)
But the CAT function is not valid there. You could technically use %SYSFUNC() to use the CAT function but there's an easier way.
%let term=201610;
%let emp='bob';
Proc Datasets library=work;
change testset = &emp&term.;
run;quit;
Note that PROC DATASETS requires a QUIT to terminate.

Get case sensitive list of macros in program

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.

Stop sas macro from overwriting different imported csv files as the same sas dataset

I found a macro and have been using it to import datasets that are given to me in csv format. Now I need to edit it because I have datasets that have an id number in them and I want sas datasets with the same name.
THE csvs are named things like IDSTUDY233_first.csv So I want the sas dataset to be IDSTUDY233_first. It should appear in my work folder.
I thought it would just create a sas dataset for each csv named IDSTUDY233_first or something like that. (and so on and so forth for each additional study). However it's naming this way.
IDSTUDY_FIRST
and over rights itself for every ID. I am newer to macros and have been trying to figure out WHY it does this and how to fix it. Suggestions?
%let subdir=Y:\filepath\; *MACRO VARIABLE FOR FILEPATH;
filename dir "&subdir.*.csv "; *give the file the name from the path that your at whatever the csv is named;
data new; *create the dataset new it has all those filepath names csv names;
length filename fname $ 200;
infile dir eof=last filename=fname;
input ;
last: filename=fname;
run;
proc sort data=new nodupkey; *sort but don't keep duplicate files;
by filename;
run;
data null; *create the dataset null;
set new;
call symputx(cats('filename',_n_),filename); *call the file name for this observation n;
call symputx(cats('dsn',_n_),compress(scan(filename,-2,'\.'), ,'ka')); *call the dataset for this file compress then read the file;
call symputx('nobs',_n_); *call for the number of observations;
run;
%put &nobs.; *but each observation in;
%macro import; *start the macro import;
%do i=1 %to &nobs; *Do for each fie to number of observations;
proc import datafile="&&filename&i" out=&&dsn&i dbms=csv replace;
getnames=yes;
run;
%end;
%mend import;
%import
*call import macro;
As you can see I added my comments of my understanding. Like I said macros are new to me. I may be incorrect in my understanding. I am guessing the problem is either in
call symputx(cats('dsn',_n_),compress(scan(filename,-2,'\.'), ,'ka'));
or it is in the import statement probably out=&&dsn&i since it rapidly over writes the previous SAS files until it does every one. It's just I need all the sas files not just the last 1.
My guess is that you are right, it is to do with this line:
call symputx(cats('dsn',_n_),compress(scan(filename,-2,'\.'), ,'ka'));
The gotcha is in the arguments passed to compress. Compress can be used to remove or keep certain characters in a string. In the above example, they are using it to just keep alphabetic characters by passing in the 'ka' modifiers. This is effectively causing files with different names (because they have different numbers) to be treated as the same file.
You can modify this behaviour to keep alphabetic characters, digits, and the underscore character by changing the parameters from ka to kn.
This change does mean that you also need to make sure that none of your file names begin with a number (as SAS datasets can't begin with a number).
The documentation for the compress function is here:
http://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000212246.htm
An easy way to debug this would be to take the dataset with all of the call symput statements, and in addition to storing these values in macro variables, write them to variables in the dataset. Also change it from a data _null_ to a data tmp statement. You can then see for each file what the destination table name will be.

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;