1
0
mirror of https://github.com/sasjs/core.git synced 2026-01-14 03:50:06 +00:00

feat(*): recreate library as scoped package

This commit is contained in:
Krishna Acondy
2020-07-07 21:27:24 +01:00
commit 8beb0048a3
144 changed files with 30478 additions and 0 deletions

141
base/mf_abort.sas Normal file
View File

@@ -0,0 +1,141 @@
/**
@file
@brief abort gracefully according to context
@details Do not use directly! See bottom of explanation for details.
Configures an abort mechanism according to site specific policies or the
particulars of an environment. For instance, can stream custom
results back to the client in an STP Web App context, or completely stop
in the case of a batch run.
For the sharp eyed readers - this is no longer a macro function!! It became
a macro procedure during a project and now it's kinda stuck that way until
that project is updated (if it's ever updated). In the meantime we created
`mp_abort` which is just a wrapper for this one, and so we recomend you use
that for forwards compatibility reasons.
@param mac= to contain the name of the calling macro
@param type= deprecated. Not used.
@param msg= message to be returned
@param iftrue= supply a condition under which the macro should be executed.
@version 9.2
@author Allan Bowe
**/
%macro mf_abort(mac=mf_abort.sas, type=, msg=, iftrue=%str(1=1)
)/*/STORE SOURCE*/;
%if not(%eval(%unquote(&iftrue))) %then %return;
%put NOTE: /// mf_abort macro executing //;
%if %length(&mac)>0 %then %put NOTE- called by &mac;
%put NOTE - &msg;
/* Stored Process Server web app context */
%if %symexist(_metaperson) or "&SYSPROCESSNAME"="Compute Server" %then %do;
options obs=max replace nosyntaxcheck mprint;
/* extract log err / warn, if exist */
%local logloc logline;
%global logmsg; /* capture global messages */
%if %symexist(SYSPRINTTOLOG) %then %let logloc=&SYSPRINTTOLOG;
%else %let logloc=%qsysfunc(getoption(LOG));
proc printto log=log;run;
%if %length(&logloc)>0 %then %do;
%let logline=0;
data _null_;
infile &logloc lrecl=5000;
input; putlog _infile_;
i=1;
retain logonce 0;
if (_infile_=:"%str(WARN)ING" or _infile_=:"%str(ERR)OR") and logonce=0 then do;
call symputx('logline',_n_);
logonce+1;
end;
run;
/* capture log including lines BEFORE the err */
%if &logline>0 %then %do;
data _null_;
infile &logloc lrecl=5000;
input;
i=1;
stoploop=0;
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
input;
i+1;
stoploop=1;
end;
if stoploop=1 then stop;
run;
%end;
%end;
/* send response in SASjs JSON format */
data _null_;
file _webout mod lrecl=32000;
length msg $32767;
sasdatetime=datetime();
msg=cats(symget('msg'),'\n\nLog Extract:\n',symget('logmsg'));
/* escape the quotes */
msg=tranwrd(msg,'"','\"');
/* ditch the CRLFs as chrome complains */
msg=compress(msg,,'kw');
/* quote without quoting the quotes (which are escaped instead) */
msg=cats('"',msg,'"');
if symexist('_debug') then debug=symget('_debug');
if debug ge 131 then put '>>weboutBEGIN<<';
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
put ',"sasjsAbort" : [{';
put ' "MSG":' msg ;
put ' ,"MAC": "' "&mac" '"}]';
put ",""SYSUSERID"" : ""&sysuserid"" ";
if symexist('_metauser') then do;
_METAUSER=quote(trim(symget('_METAUSER')));
put ",""_METAUSER"": " _METAUSER;
_METAPERSON=quote(trim(symget('_METAPERSON')));
put ',"_METAPERSON": ' _METAPERSON;
end;
_PROGRAM=quote(trim(resolve(symget('_PROGRAM'))));
put ',"_PROGRAM" : ' _PROGRAM ;
put ",""SYSCC"" : ""&syscc"" ";
put ",""SYSERRORTEXT"" : ""&syserrortext"" ";
put ",""SYSJOBID"" : ""&sysjobid"" ";
put ",""SYSWARNINGTEXT"" : ""&syswarningtext"" ";
put ',"END_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '" ';
put "}" @;
%if &_debug ge 131 %then %do;
put '>>weboutEND<<';
%end;
run;
%let syscc=0;
%if %symexist(SYS_JES_JOB_URI) %then %do;
/* refer web service output to file service in one hit */
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name="_webout.json";
%let rc=%sysfunc(fcopy(_web,_webout));
%end;
%else %do;
data _null_;
if symexist('sysprocessmode')
then if symget("sysprocessmode")="SAS Stored Process Server"
then rc=stpsrvset('program error', 0);
run;
%end;
/**
* endsas is reliable but kills some deployments.
* Abort variants are ungraceful (non zero return code)
* This approach lets SAS run silently until the end :-)
*/
%put _all_;
filename skip temp;
data _null_;
file skip;
put '%macro skip(); %macro skippy();';
run;
%inc skip;
%end;
%else %do;
%put _all_;
%abort cancel;
%end;
%mend;

26
base/mf_existds.sas Executable file
View File

@@ -0,0 +1,26 @@
/**
@file mf_existds.sas
@brief Checks whether a dataset OR a view exists.
@details Can be used in open code, eg as follows:
%if %mf_existds(libds=work.someview) %then %put yes it does!;
NOTE - some databases have case sensitive tables, for instance POSTGRES
with the preserve_tab_names=yes libname setting. This may impact
expected results (depending on whether you 'expect' the result to be
case insensitive in this context!)
@param libds library.dataset
@return output returns 1 or 0
@warning Untested on tables registered in metadata but not physically present
@version 9.2
@author Allan Bowe
**/
%macro mf_existds(libds
)/*/STORE SOURCE*/;
%if %sysfunc(exist(&libds)) ne 1 & %sysfunc(exist(&libds,VIEW)) ne 1 %then 0;
%else 1;
%mend;

42
base/mf_existfeature.sas Normal file
View File

@@ -0,0 +1,42 @@
/**
@file mf_existfeature.sas
@brief Checks whether a feature exists
@details Check to see if a feature is supported in your environment.
Run without arguments to see a list of detectable features.
Note - this list is based on known versions of SAS rather than
actual feature detection, as that is tricky / impossible to do
without generating errors in most cases.
%put %mf_existfeature(PROCLUA);
@param feature the feature to detect. Leave blank to list all in log.
@return output returns 1 or 0 (or -1 if not found)
<h4> Dependencies </h4>
@li mf_getplatform.sas
@version 8
@author Allan Bowe
**/
%macro mf_existfeature(feature
)/*/STORE SOURCE*/;
%let feature=%upcase(&feature);
%local platform;
%let platform=%mf_getplatform();
%if &feature= %then %do;
%put Supported features: PROCLUA;
%end;
%else %if &feature=PROCLUA %then %do;
%if &platform=SASVIYA %then 1;
%else %if "&sysver"="9.3" or "&sysver"="9.4" %then 1;
%else 0;
%end;
%else %do;
-1
%put &sysmacroname: &feature not found;
%end;
%mend;

32
base/mf_existvar.sas Executable file
View File

@@ -0,0 +1,32 @@
/**
@file
@brief Checks if a variable exists in a data set.
@details Returns 0 if the variable does NOT exist, and return the position of
the var if it does.
Usage:
%put %mf_existvar(work.someds, somevar)
@param libds (positional) - 2 part dataset or view reference
@param var (positional) - variable name
@version 9.2
@author Allan Bowe
**/
%macro mf_existvar(libds /* 2 part dataset name */
, var /* variable name */
)/*/STORE SOURCE*/;
%local dsid rc;
%let dsid=%sysfunc(open(&libds,is));
%if &dsid=0 or %length(&var)=0 %then %do;
%put %sysfunc(sysmsg());
0
%end;
%else %do;
%sysfunc(varnum(&dsid,&var))
%let rc=%sysfunc(close(&dsid));
%end;
%mend;

56
base/mf_existvarlist.sas Executable file
View File

@@ -0,0 +1,56 @@
/**
@file
@brief Checks if a set of variables ALL exist in a data set.
@details Returns 0 if ANY of the variables do not exist, or 1 if they ALL do.
Usage:
%put %mf_existVarList(sashelp.class, age sex name dummyvar)
<h4> Dependencies </h4>
@li mf_abort.sas
@param libds 2 part dataset or view reference
@param varlist space separated variable names
@version 9.2
@author Allan Bowe
**/
%macro mf_existvarlist(libds, varlist
)/*/STORE SOURCE*/;
%if %str(&libds)=%str() or %str(&varlist)=%str() %then %do;
%mf_abort(msg=No value provided to libds(&libds) or varlist (&varlist)!
,mac=mf_existvarlist.sas)
%end;
%local dsid rc i var found;
%let dsid=%sysfunc(open(&libds,is));
%if &dsid=0 %then %do;
%put WARNING: unable to open &libds in mf_existvarlist (&dsid);
%end;
%if %sysfunc(attrn(&dsid,NVARS))=0 %then %do;
%put MF_EXISTVARLIST: No variables in &libds ;
0
%return;
%end;
%else %do i=1 %to %sysfunc(countw(&varlist));
%let var=%scan(&varlist,&i);
%if %sysfunc(varnum(&dsid,&var))=0 %then %do;
%let found=&found &var;
%end;
%end;
%let rc=%sysfunc(close(&dsid));
%if %str(&found)=%str() %then %do;
1
%end;
%else %do;
0
%put Vars not found: &found;
%end;
%mend;

34
base/mf_getattrc.sas Normal file
View File

@@ -0,0 +1,34 @@
/**
@file
@brief Returns a character attribute of a dataset.
@details Can be used in open code, eg as follows:
%put Dataset label = %mf_getattrc(sashelp.class,LABEL);
%put Member Type = %mf_getattrc(sashelp.class,MTYPE);
@param libds library.dataset
@param attr full list in [documentation](
https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000147794.htm)
@return output returns result of the attrc value supplied, or -1 and log
message if error.
@version 9.2
@author Allan Bowe
**/
%macro mf_getattrc(
libds
,attr
)/*/STORE SOURCE*/;
%local dsid rc;
%let dsid=%sysfunc(open(&libds,is));
%if &dsid = 0 %then %do;
%put WARNING: Cannot open %trim(&libds), system message below;
%put %sysfunc(sysmsg());
-1
%end;
%else %do;
%sysfunc(attrc(&dsid,&attr))
%let rc=%sysfunc(close(&dsid));
%end;
%mend;

34
base/mf_getattrn.sas Executable file
View File

@@ -0,0 +1,34 @@
/**
@file
@brief Returns a numeric attribute of a dataset.
@details Can be used in open code, eg as follows:
%put Number of observations=%mf_getattrn(sashelp.class,NLOBS);
%put Number of variables = %mf_getattrn(sashelp.class,NVARS);
@param libds library.dataset
@param attr Common values are NLOBS and NVARS, full list in [documentation](
http://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000212040.htm)
@return output returns result of the attrn value supplied, or -1 and log
message if error.
@version 9.2
@author Allan Bowe
**/
%macro mf_getattrn(
libds
,attr
)/*/STORE SOURCE*/;
%local dsid rc;
%let dsid=%sysfunc(open(&libds,is));
%if &dsid = 0 %then %do;
%put WARNING: Cannot open %trim(&libds), system message below;
%put %sysfunc(sysmsg());
-1
%end;
%else %do;
%sysfunc(attrn(&dsid,&attr))
%let rc=%sysfunc(close(&dsid));
%end;
%mend;

44
base/mf_getengine.sas Executable file
View File

@@ -0,0 +1,44 @@
/**
@file
@brief Returns the engine type of a SAS library
@details Usage:
%put %mf_getEngine(SASHELP);
returns:
> V9
A note is also written to the log. The credit for this macro goes to the
contributors of Chris Hemedingers blog [post](
http://blogs.sas.com/content/sasdummy/2013/06/04/find-a-sas-library-engine/)
@param libref Library reference (also accepts a 2 level libds ref).
@return output returns the library engine for the FIRST library encountered.
@warning will only return the FIRST library engine - for concatenated
libraries, with different engines, inconsistent results may be encountered.
@version 9.2
@author Allan Bowe
**/
%macro mf_getEngine(libref
)/*/STORE SOURCE*/;
%local dsid engnum rc engine;
/* in case the parameter is a libref.tablename, pull off just the libref */
%let libref = %upcase(%scan(&libref, 1, %str(.)));
%let dsid=%sysfunc(open(sashelp.vlibnam(where=(libname="%upcase(&libref)")),i));
%if (&dsid ^= 0) %then %do;
%let engnum=%sysfunc(varnum(&dsid,ENGINE));
%let rc=%sysfunc(fetch(&dsid));
%let engine=%sysfunc(getvarc(&dsid,&engnum));
%put &libref. ENGINE is &engine.;
%let rc= %sysfunc(close(&dsid));
%end;
&engine
%mend;

47
base/mf_getfilesize.sas Normal file
View File

@@ -0,0 +1,47 @@
/**
@file
@brief Returns the size of a file in bytes.
@details Provide full path/filename.extension to the file, eg:
%put %mf_getfilesize(fpath=C:\temp\myfile.txt);
or
data x;do x=1 to 100000;y=x;output;end;run;
%put %mf_getfilesize(libds=work.x,format=yes);
gives:
2mb
@param fpath= full path and filename. Provide this OR the libds value.
@param libds= library.dataset value (assumes library is BASE engine)
@param format= set to yes to apply sizekmg. format
@returns bytes
@version 9.2
@author Allan Bowe
**/
%macro mf_getfilesize(fpath=,libds=0,format=NO
)/*/STORE SOURCE*/;
%if &libds ne 0 %then %do;
%let fpath=%sysfunc(pathname(%scan(&libds,1,.)))/%scan(&libds,2,.).sas7bdat;
%end;
%local rc fid fref bytes;
%let rc=%sysfunc(filename(fref,&fpath));
%let fid=%sysfunc(fopen(&fref));
%let bytes=%sysfunc(finfo(&fid,File Size (bytes)));
%let rc=%sysfunc(fclose(&fid));
%let rc=%sysfunc(filename(fref));
%if &format=NO %then %do;
&bytes
%end;
%else %do;
%sysfunc(INPUTN(&bytes, best.),sizekmg.)
%end;
%mend ;

32
base/mf_getkeyvalue.sas Normal file
View File

@@ -0,0 +1,32 @@
/**
@file
@brief retrieves a key value pair from a control dataset
@details By default, control dataset is work.mp_setkeyvalue. Usage:
%mp_setkeyvalue(someindex,22,type=N)
%put %mf_getkeyvalue(someindex)
@param key Provide a key on which to perform the lookup
@param libds= define the target table which holds the parameters
@version 9.2
@author Allan Bowe
**/
%macro mf_getkeyvalue(key,libds=work.mp_setkeyvalue
)/*/STORE SOURCE*/;
%local ds dsid key valc valn type rc;
%let dsid=%sysfunc(open(&libds(where=(key="&key"))));
%syscall set(dsid);
%let rc = %sysfunc(fetch(&dsid));
%let rc = %sysfunc(close(&dsid));
%if &type=N %then %do;
&valn
%end;
%else %if &type=C %then %do;
&valc
%end;
%else %put %str(ERR)OR: Unable to find key &key in ds &libds;
%mend;

62
base/mf_getplatform.sas Normal file
View File

@@ -0,0 +1,62 @@
/**
@file mf_getplatform
@brief Returns platform specific variables
@details Enables platform specific variables to be returned
%put %mf_getplatform();
returns:
SASMETA (or SASVIYA)
@param switch the param for which to return a platform specific variable
<h4> Dependencies </h4>
@li mf_mval.sas
@version 9.4 / 3.4
@author Allan Bowe
**/
%macro mf_getplatform(switch
)/*/STORE SOURCE*/;
%local a b c;
%if &switch.NONE=NONE %then %do;
%if %symexist(sysprocessmode) %then %do;
%if "&sysprocessmode"="SAS Object Server"
or "&sysprocessmode"= "SAS Compute Server" %then %do;
SASVIYA
%end;
%else %if "&sysprocessmode"="SAS Stored Process Server" %then %do;
SASMETA
%return;
%end;
%else %do;
SAS
%return;
%end;
%end;
%else %if %symexist(_metaport) %then %do;
SASMETA
%return;
%end;
%else %do;
SAS
%return;
%end;
%end;
%else %if &switch=SASSTUDIO %then %do;
/* return the version of SAS Studio else 0 */
%if %mf_mval(_CLIENTAPP)=%str(SAS Studio) %then %do;
%let a=%mf_mval(_CLIENTVERSION);
%let b=%scan(&a,1,.);
%if %eval(&b >2) %then %do;
&b
%end;
%else 0;
%end;
%else 0;
%end;
%else %if &switch=VIYARESTAPI %then %do;
%sysfunc(getoption(servicesbaseurl))
%end;
%mend;

46
base/mf_getquotedstr.sas Executable file
View File

@@ -0,0 +1,46 @@
/**
@file
@brief Adds custom quotes / delimiters to a delimited string
@details Can be used in open code, eg as follows:
%put %mf_getquotedstr(blah blah blah);
which returns:
> 'blah','blah','blah'
@param in_str the unquoted, spaced delimited string to transform
@param dlm= the delimeter to be applied to the output (default comma)
@param indlm= the delimeter used for the input (default is space)
@param quote= the quote mark to apply (S=Single, D=Double). If any other value
than uppercase S or D is supplied, then that value will be used as the
quoting character.
@return output returns a string with the newly quoted / delimited output.
@version 9.2
@author Allan Bowe
**/
%macro mf_getquotedstr(IN_STR,DLM=%str(,),QUOTE=S,indlm=%str( )
)/*/STORE SOURCE*/;
%if &quote=S %then %let quote=%str(%');
%else %if &quote=D %then %let quote=%str(%");
%else %let quote=%str();
%local i item buffer;
%let i=1;
%do %while (%qscan(&IN_STR,&i,%str(&indlm)) ne %str() ) ;
%let item=%qscan(&IN_STR,&i,%str(&indlm));
%if %bquote(&QUOTE) ne %then %let item=&QUOTE%qtrim(&item)&QUOTE;
%else %let item=%qtrim(&item);
%if (&i = 1) %then %let buffer =%qtrim(&item);
%else %let buffer =&buffer&DLM%qtrim(&item);
%let i = %eval(&i+1);
%end;
%let buffer=%sysfunc(coalescec(%qtrim(&buffer),&QUOTE&QUOTE));
&buffer
%mend;

40
base/mf_getschema.sas Normal file
View File

@@ -0,0 +1,40 @@
/**
@file mf_getschema.sas
@brief Returns the database schema of a SAS library
@details Usage:
%put %mf_getschema(MYDB);
returns:
> dbo
@param libref Library reference (also accepts a 2 level libds ref).
@return output returns the library schema for the FIRST library encountered
@warning will only return the FIRST library schema - for concatenated
libraries, with different schemas, inconsistent results may be encountered.
@version 9.2
@author Allan Bowe
**/
%macro mf_getschema(libref
)/*/STORE SOURCE*/;
%local dsid vnum rc schema;
/* in case the parameter is a libref.tablename, pull off just the libref */
%let libref = %upcase(%scan(&libref, 1, %str(.)));
%let dsid=%sysfunc(open(sashelp.vlibnam(where=(
libname="%upcase(&libref)" and sysname='Schema/Owner'
)),i));
%if (&dsid ^= 0) %then %do;
%let vnum=%sysfunc(varnum(&dsid,SYSVALUE));
%let rc=%sysfunc(fetch(&dsid));
%let schema=%sysfunc(getvarc(&dsid,&vnum));
%put &libref. schema is &schema.;
%let rc= %sysfunc(close(&dsid));
%end;
&schema
%mend;

View File

@@ -0,0 +1,36 @@
/**
@file
@brief Assigns and returns an unused fileref
@details Use as follows:
%let fileref1=%mf_getuniquefileref();
%let fileref2=%mf_getuniquefileref();
%put &fileref1 &fileref2;
which returns:
> mcref0 mcref1
@prefix= first part of fileref. Remember that filerefs can only be 8
characters, so a 7 letter prefix would mean that `maxtries` should be 10.
@param maxtries= the last part of the libref. Provide an integer value.
@version 9.2
@author Allan Bowe
**/
%macro mf_getuniquefileref(prefix=mcref,maxtries=1000);
%local x fname;
%let x=0;
%do x=0 %to &maxtries;
%if %sysfunc(fileref(&prefix&x)) > 0 %then %do;
%let fname=&prefix&x;
%let rc=%sysfunc(filename(fname,,temp));
%if &rc %then %put %sysfunc(sysmsg());
&prefix&x
%*put &sysmacroname: Fileref &prefix&x was assigned and returned;
%return;
%end;
%end;
%put unable to find available fileref in range &prefix.0-&maxtries;
%mend;

View File

@@ -0,0 +1,40 @@
/**
@file
@brief Returns an unused libref
@details Use as follows:
libname mclib0 (work);
libname mclib1 (work);
libname mclib2 (work);
%let libref=%mf_getuniquelibref();
%put &=libref;
which returns:
> mclib3
@prefix= first part of libref. Remember that librefs can only be 8 characters,
so a 7 letter prefix would mean that maxtries should be 10.
@param maxtries= the last part of the libref. Provide an integer value.
@version 9.2
@author Allan Bowe
**/
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x=0;
%do x=0 %to &maxtries;
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
%let libref=&prefix&x;
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
%if &rc %then %put %sysfunc(sysmsg());
&prefix&x
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
%return;
%end;
%end;
%put unable to find available libref in range &prefix.0-&maxtries;
%mend;

22
base/mf_getuniquename.sas Normal file
View File

@@ -0,0 +1,22 @@
/**
@file mf_getuniquename.sas
@brief Returns a shortened (32 char) GUID as a valid SAS name
@details Use as follows:
%let myds=%mf_getuniquename();
%put &=myds;
which returns:
> MCc59c750610321d4c8bf75faadbcd22
@param prefix= set a prefix for the new name
@version 9.3
@author Allan Bowe
**/
%macro mf_getuniquename(prefix=MC);
&prefix.%substr(%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32-%length(&prefix))
%mend;

42
base/mf_getuser.sas Executable file
View File

@@ -0,0 +1,42 @@
/**
@file
@brief Returns a userid according to session context
@details In a workspace session, a user is generally represented by <code>
&sysuserid</code> or <code>SYS_COMPUTE_SESSION_OWNER</code> if it exists.
In a Stored Process session, <code>&sysuserid</code>
resolves to a system account (default=sassrv) and instead there are several
metadata username variables to choose from (_metauser, _metaperson
,_username, _secureusername). The OS account is represented by
<code> _secureusername</code> whilst the metadata account is under <code>
_metaperson</code>.
%let user= %mf_getUser();
%put &user;
@param type - do not use, may be deprecated in a future release
@return SYSUSERID (if workspace server)
@return _METAPERSON (if stored process server)
@return SYS_COMPUTE_SESSION_OWNER (if Viya compute session)
@version 9.2
@author Allan Bowe
**/
%macro mf_getuser(type=META
)/*/STORE SOURCE*/;
%local user metavar;
%if &type=OS %then %let metavar=_secureusername;
%else %let metavar=_metaperson;
%if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %let user=&SYS_COMPUTE_SESSION_OWNER;
%else %if %symexist(&metavar) %then %do;
%if %length(&&&metavar)=0 %then %let user=&sysuserid;
/* sometimes SAS will add @domain extension - remove for consistency */
%else %let user=%scan(&&&metavar,1,@);
%end;
%else %let user=&sysuserid;
%quote(&user)
%mend;

33
base/mf_getvalue.sas Normal file
View File

@@ -0,0 +1,33 @@
/**
@file
@brief Retrieves a value from a dataset. If no filter supplied, then first
record is used.
@details Be sure to <code>%quote()</code> your where clause. Example usage:
%put %mf_getvalue(sashelp.class,name,filter=%quote(age=15));
%put %mf_getvalue(sashelp.class,name);
<h4> Dependencies </h4>
@li mf_getattrn.sas
@param libds dataset to query
@param variable the variable which contains the value to return.
@param filter contents of where clause
@version 9.2
@author Allan Bowe
**/
%macro mf_getvalue(libds,variable,filter=1
)/*/STORE SOURCE*/;
%if %mf_getattrn(&libds,NLOBS)>0 %then %do;
%local dsid rc &variable;
%let dsid=%sysfunc(open(&libds(where=(&filter))));
%syscall set(dsid);
%let rc = %sysfunc(fetch(&dsid));
%let rc = %sysfunc(close(&dsid));
%trim(&&&variable)
%end;
%mend;

32
base/mf_getvarcount.sas Normal file
View File

@@ -0,0 +1,32 @@
/**
@file
@brief Returns number of variables in a dataset
@details Useful to identify those renagade datasets that have no columns!
%put Number of Variables=%mf_getvarcount(sashelp.class);
returns:
> Number of Variables=4
@param libds Two part dataset (or view) reference.
@version 9.2
@author Allan Bowe
**/
%macro mf_getvarcount(libds
)/*/STORE SOURCE*/;
%local dsid nvars rc ;
%let dsid=%sysfunc(open(&libds));
%let nvars=.;
%if &dsid %then %do;
%let nvars=%sysfunc(attrn(&dsid,NVARS));
%let rc=%sysfunc(close(&dsid));
%end;
%else %do;
%put unable to open &libds (rc=&dsid);
%let rc=%sysfunc(close(&dsid));
%end;
&nvars
%mend;

71
base/mf_getvarformat.sas Executable file
View File

@@ -0,0 +1,71 @@
/**
@file
@brief Returns the format of a variable
@details Uses varfmt function to identify the format of a particular variable.
Usage:
data test;
format str1 $1. num1 datetime19.;
str2='hello mum!'; num2=666;
stop;
run;
%put %mf_getVarFormat(test,str1);
%put %mf_getVarFormat(work.test,num1);
%put %mf_getVarFormat(test,str2,force=1);
%put %mf_getVarFormat(work.test,num2,force=1);
%put %mf_getVarFormat(test,renegade);
returns:
$1.
DATETIME19.
$10.
8.
NOTE: Variable renegade does not exist in test
@param libds Two part dataset (or view) reference.
@param var Variable name for which a format should be returned
@param force Set to 1 to supply a default if the variable has no format
@returns outputs format
@author Allan Bowe
@version 9.2
**/
%macro mf_getVarFormat(libds /* two level ds name */
, var /* variable name from which to return the format */
, force=0
)/*/STORE SOURCE*/;
%local dsid vnum vformat rc vlen vtype;
/* Open dataset */
%let dsid = %sysfunc(open(&libds));
%if &dsid > 0 %then %do;
/* Get variable number */
%let vnum = %sysfunc(varnum(&dsid, &var));
/* Get variable format */
%if(&vnum > 0) %then %let vformat=%sysfunc(varfmt(&dsid, &vnum));
%else %do;
%put NOTE: Variable &var does not exist in &libds;
%let rc = %sysfunc(close(&dsid));
%return;
%end;
%end;
%else %do;
%put dataset &libds not opened! (rc=&dsid);
%return;
%end;
/* supply a default if no format available */
%if %length(&vformat)<2 & &force=1 %then %do;
%let vlen = %sysfunc(varlen(&dsid, &vnum));
%let vtype = %sysfunc(vartype(&dsid, &vnum.));
%if &vtype=C %then %let vformat=$&vlen..;
%else %let vformat=8.;
%end;
/* Close dataset */
%let rc = %sysfunc(close(&dsid));
/* Return variable format */
&vformat
%mend;

52
base/mf_getvarlen.sas Normal file
View File

@@ -0,0 +1,52 @@
/**
@file
@brief Returns the length of a variable
@details Uses varlen function to identify the length of a particular variable.
Usage:
data test;
format str $1. num datetime19.;
stop;
run;
%put %mf_getVarLen(test,str);
%put %mf_getVarLen(work.test,num);
%put %mf_getVarLen(test,renegade);
returns:
1
8
NOTE: Variable renegade does not exist in test
@param libds Two part dataset (or view) reference.
@param var Variable name for which a length should be returned
@returns outputs length
@author Allan Bowe
@version 9.2
**/
%macro mf_getVarLen(libds /* two level ds name */
, var /* variable name from which to return the length */
)/*/STORE SOURCE*/;
%local dsid vnum vlen rc;
/* Open dataset */
%let dsid = %sysfunc(open(&libds));
%if &dsid > 0 %then %do;
/* Get variable number */
%let vnum = %sysfunc(varnum(&dsid, &var));
/* Get variable format */
%if(&vnum > 0) %then %let vlen = %sysfunc(varlen(&dsid, &vnum));
%else %do;
%put NOTE: Variable &var does not exist in &libds;
%let vlen = %str( );
%end;
%end;
%else %put dataset &libds not opened! (rc=&dsid);
/* Close dataset */
%let rc = %sysfunc(close(&dsid));
/* Return variable format */
&vlen
%mend;

64
base/mf_getvarlist.sas Executable file
View File

@@ -0,0 +1,64 @@
/**
@file
@brief Returns dataset variable list direct from header
@details WAY faster than dictionary tables or sas views, and can
also be called in macro logic (is pure macro). Can be used in open code,
eg as follows:
%put List of Variables=%mf_getvarlist(sashelp.class);
returns:
> List of Variables=Name Sex Age Height Weight
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
returns:
> "Name","Sex","Age","Height","Weight"
@param libds Two part dataset (or view) reference.
@param dlm= provide a delimiter (eg comma or space) to separate the vars
@param quote= use either DOUBLE or SINGLE to quote the results
@version 9.2
@author Allan Bowe
**/
%macro mf_getvarlist(libds
,dlm=%str( )
,quote=no
)/*/STORE SOURCE*/;
/* declare local vars */
%local outvar dsid nvars x rc dlm q var;
/* credit Rowland Hale - byte34 is double quote, 39 is single quote */
%if %upcase(&quote)=DOUBLE %then %let q=%qsysfunc(byte(34));
%else %if %upcase(&quote)=SINGLE %then %let q=%qsysfunc(byte(39));
/* open dataset in macro */
%let dsid=%sysfunc(open(&libds));
%if &dsid %then %do;
%let nvars=%sysfunc(attrn(&dsid,NVARS));
%if &nvars>0 %then %do;
/* add first dataset variable to global macro variable */
%let outvar=&q.%sysfunc(varname(&dsid,1))&q.;
/* add remaining variables with supplied delimeter */
%do x=1 %to &nvars;
%let var=&q.%sysfunc(varname(&dsid,&x))&q.;
%if &var=&q&q %then %do;
%put &sysmacroname: Empty column found in &libds!;
%let var=&q. &q.;
%end;
%if &x=1 %then %let outvar=&var;
%else %let outvar=&outvar.&dlm.&var.;
%end;
%end;
%let rc=%sysfunc(close(&dsid));
%end;
%else %do;
%put unable to open &libds (rc=&dsid);
%let rc=%sysfunc(close(&dsid));
%end;
&outvar
%mend;

54
base/mf_getvarnum.sas Executable file
View File

@@ -0,0 +1,54 @@
/**
@file
@brief Returns the position of a variable in dataset (varnum attribute).
@details Uses varnum function to determine position.
Usage:
data work.test;
format str $1. num datetime19.;
stop;
run;
%put %mf_getVarNum(work.test,str);
%put %mf_getVarNum(work.test,num);
%put %mf_getVarNum(work.test,renegade);
returns:
> 1
> 2
> NOTE: Variable renegade does not exist in test
@param libds Two part dataset (or view) reference.
@param var Variable name for which a position should be returned
@author Allan Bowe
@version 9.2
**/
%macro mf_getVarNum(libds /* two level ds name */
, var /* variable name from which to return the format */
)/*/STORE SOURCE*/;
%local dsid vnum rc;
/* Open dataset */
%let dsid = %sysfunc(open(&libds));
%if &dsid > 0 %then %do;
/* Get variable number */
%let vnum = %sysfunc(varnum(&dsid, &var));
%if(&vnum <= 0) %then %do;
%put NOTE: Variable &var does not exist in &libds;
%let vnum = %str( );
%end;
%end;
%else %put dataset &ds not opened! (rc=&dsid);
/* Close dataset */
%let rc = %sysfunc(close(&dsid));
/* Return variable number */
&vnum.
%mend;

48
base/mf_getvartype.sas Executable file
View File

@@ -0,0 +1,48 @@
/**
@file
@brief Returns variable type - Character (C) or Numeric (N)
@details
Usage:
data test;
length str $1. num 8.;
stop;
run;
%put %mf_getvartype(test,str);
%put %mf_getvartype(work.test,num);
@param libds Two part dataset (or view) reference.
@param var the variable name to be checked
@return output returns C or N depending on variable type. If variable
does not exist then a blank is returned and a note is written to the log.
@version 9.2
@author Allan Bowe
**/
%macro mf_getvartype(libds /* two level name */
, var /* variable name from which to return the type */
)/*/STORE SOURCE*/;
%local dsid vnum vtype rc;
/* Open dataset */
%let dsid = %sysfunc(open(&libds));
%if &dsid. > 0 %then %do;
/* Get variable number */
%let vnum = %sysfunc(varnum(&dsid, &var));
/* Get variable type (C/N) */
%if(&vnum. > 0) %then %let vtype = %sysfunc(vartype(&dsid, &vnum.));
%else %do;
%put NOTE: Variable &var does not exist in &libds;
%let vtype = %str( );
%end;
%end;
%else %put dataset &libds not opened! (rc=&dsid);
/* Close dataset */
%let rc = %sysfunc(close(&dsid));
/* Return variable type */
&vtype
%mend;

26
base/mf_isblank.sas Normal file
View File

@@ -0,0 +1,26 @@
/**
@file mf_isblank.sas
@brief Checks whether a macro variable is empty (blank)
@details Simply performs:
%sysevalf(%superq(param)=,boolean)
Usage:
%put mf_isblank(&var);
inspiration: https://support.sas.com/resources/papers/proceedings09/022-2009.pdf
@param param VALUE to be checked
@return output returns 1 (if blank) else 0
@version 9.2
**/
%macro mf_isblank(param
)/*/STORE SOURCE*/;
%sysevalf(%superq(param)=,boolean)
%mend;

29
base/mf_loc.sas Normal file
View File

@@ -0,0 +1,29 @@
/**
@file
@brief Returns physical location of various SAS items
@details Returns location of the PlatformObjectFramework tools
Usage:
%put %mf_loc(POF); %*location of PlatformObjectFramework tools;
@version 9.2
@author Allan Bowe
**/
%macro mf_loc(loc);
%let loc=%upcase(&loc);
%local root;
%if &loc=POF or &loc=PLATFORMOBJECTFRAMEWORK %then %do;
%let root=%substr(%sysget(SASROOT),1,%index(%sysget(SASROOT),SASFoundation)-2);
%let root=&root/SASPlatformObjectFramework/&sysver;
%put Batch tools located at: &root;
&root
%end;
%else %if &loc=VIYACONFIG %then %do;
%let root=/opt/sas/viya/config;
%put Viya Config located at: &root;
&root
%end;
%mend;

67
base/mf_mkdir.sas Executable file
View File

@@ -0,0 +1,67 @@
/**
@file
@brief Creates a directory, including any intermediate directories
@details Works on windows and unix environments via dcreate function.
Usage:
%mf_mkdir(/some/path/name)
@param dir relative or absolute pathname. Unquoted.
@version 9.2
**/
%macro mf_mkdir(dir
)/*/STORE SOURCE*/;
%local lastchar child parent;
%let lastchar = %substr(&dir, %length(&dir));
%if (%bquote(&lastchar) eq %str(:)) %then %do;
/* Cannot create drive mappings */
%return;
%end;
%if (%bquote(&lastchar)=%str(/)) or (%bquote(&lastchar)=%str(\)) %then %do;
/* last char is a slash */
%if (%length(&dir) eq 1) %then %do;
/* one single slash - root location is assumed to exist */
%return;
%end;
%else %do;
/* strip last slash */
%let dir = %substr(&dir, 1, %length(&dir)-1);
%end;
%end;
%if (%sysfunc(fileexist(%bquote(&dir))) = 0) %then %do;
/* directory does not exist so prepare to create */
/* first get the childmost directory */
%let child = %scan(&dir, -1, %str(/\:));
/*
If child name = path name then there are no parents to create. Else
they must be recursively scanned.
*/
%if (%length(&dir) gt %length(&child)) %then %do;
%let parent = %substr(&dir, 1, %length(&dir)-%length(&child));
%mf_mkdir(&parent)
%end;
/*
Now create the directory. Complain loudly of any errors.
*/
%let dname = %sysfunc(dcreate(&child, &parent));
%if (%bquote(&dname) eq ) %then %do;
%put %str(ERR)OR: could not create &parent + &child;
%abort cancel;
%end;
%else %do;
%put Directory created: &dir;
%end;
%end;
/* exit quietly if directory did exist.*/
%mend;

18
base/mf_mval.sas Normal file
View File

@@ -0,0 +1,18 @@
/**
@file mf_mval.sas
@brief Returns a macro variable value if the variable exists
@details Use this macro to avoid repetitive use of `%if %symexist(MACVAR) %then`
type logic.
Usage:
%if %mf_mval(maynotexist)=itdid %then %do;
@version 9.2
@author Allan Bowe
**/
%macro mf_mval(var);
%if %symexist(&var) %then %do;
%superq(&var)
%end;
%mend;

26
base/mf_nobs.sas Executable file
View File

@@ -0,0 +1,26 @@
/**
@file
@brief Returns number of logical (undeleted) observations.
@details Beware - will not work on external database tables!
Is just a convenience macro for calling <code> %mf_getattrn()</code>.
%put Number of observations=%mf_nobs(sashelp.class);
<h4> Dependencies </h4>
@li mf_getattrn.sas
@param libds library.dataset
@return output returns result of the attrn value supplied, or log message
if error.
@version 9.2
@author Allan Bowe
**/
%macro mf_nobs(libds
)/*/STORE SOURCE*/;
%mf_getattrn(&libds,NLOBS)
%mend;

21
base/mf_uid.sas Normal file
View File

@@ -0,0 +1,21 @@
/**
@file
@brief Creates a Unique ID based on system time in a friendly format
@details format = YYYYMMDD_HHMMSSmmm_<sysjobid>_<3randomDigits>
%put %mf_uid();
@version 9.2
@author Allan Bowe
**/
%macro mf_uid(
)/*/STORE SOURCE*/;
%local today now;
%let today=%sysfunc(today(),yymmddn8.);
%let now=%sysfunc(compress(%sysfunc(time(),time12.3),:.));
&today._&now._&sysjobid._%sysevalf(%sysfunc(ranuni(0))*999,CEIL)
%mend;

65
base/mf_verifymacvars.sas Executable file
View File

@@ -0,0 +1,65 @@
/**
@file
@brief Checks if a set of macro variables exist / contain values.
@details Writes ERROR to log if abortType is SOFT, else will call %mf_abort.
Usage:
%let var1=x;
%let var2=y;
%put %mf_verifymacvars(var1 var2);
Returns:
> 1
<h4> Dependencies </h4>
@li mf_abort.sas
@param verifyvars space separated list of macro variable names
@param makeupcase= set to YES to convert all variable VALUES to
uppercase.
@param mAbort= Abort Type. Default is SOFT (writes err to log).
Set to any other value to call mf_abort (which can be configured to abort in
various fashions according to context).
@warning will not be able to verify the following variables due to
naming clash!
- verifyVars
- verifyVar
- verifyIterator
- makeUpcase
@version 9.2
@author Allan Bowe
**/
%macro mf_verifymacvars(
verifyVars /* list of macro variable NAMES */
,makeUpcase=NO /* set to YES to make all the variable VALUES uppercase */
,mAbort=SOFT
)/*/STORE SOURCE*/;
%local verifyIterator verifyVar abortmsg;
%do verifyIterator=1 %to %sysfunc(countw(&verifyVars,%str( )));
%let verifyVar=%qscan(&verifyVars,&verifyIterator,%str( ));
%if not %symexist(&verifyvar) %then %do;
%let abortmsg= Variable &verifyVar is MISSING;
%goto exit_err;
%end;
%if %length(%trim(&&&verifyVar))=0 %then %do;
%let abortmsg= Variable &verifyVar is EMPTY;
%goto exit_err;
%end;
%if &makeupcase=YES %then %do;
%let &verifyVar=%upcase(&&&verifyvar);
%end;
%end;
%goto exit_success;
%exit_err:
%if &mAbort=SOFT %then %put %str(ERR)OR: &abortmsg;
%else %mf_abort(mac=mf_verifymacvars,type=&mabort,msg=&abortmsg);
%exit_success:
%mend;

View File

@@ -0,0 +1,54 @@
/**
@file
@brief Returns words that are in string 1 but not in string 2
@details Compares two space separated strings and returns the words that are
in the first but not in the second.
Usage:
%let x= %mf_wordsInStr1ButNotStr2(
Str1=blah sss blaaah brah bram boo
,Str2= blah blaaah brah ssss
);
returns:
> sss bram boo
@param str1= string containing words to extract
@param str2= used to compare with the extract string
@warning CASE SENSITIVE!
@version 9.2
@author Allan Bowe
**/
%macro mf_wordsInStr1ButNotStr2(
Str1= /* string containing words to extract */
,Str2= /* used to compare with the extract string */
)/*/STORE SOURCE*/;
%local count_base count_extr i i2 extr_word base_word match outvar;
%if %length(&str1)=0 or %length(&str2)=0 %then %do;
%put WARNING: empty string provided!;
%put base string (str1)= &str1;
%put compare string (str2) = &str2;
%return;
%end;
%let count_base=%sysfunc(countw(&Str2));
%let count_extr=%sysfunc(countw(&Str1));
%do i=1 %to &count_extr;
%let extr_word=%scan(&Str1,&i,%str( ));
%let match=0;
%do i2=1 %to &count_base;
%let base_word=%scan(&Str2,&i2,%str( ));
%if &extr_word=&base_word %then %let match=1;
%end;
%if &match=0 %then %let outvar=&outvar &extr_word;
%end;
&outvar
%mend;

140
base/mp_abort.sas Normal file
View File

@@ -0,0 +1,140 @@
/**
@file
@brief abort gracefully according to context
@details Configures an abort mechanism according to site specific policies or
the particulars of an environment. For instance, can stream custom
results back to the client in an STP Web App context, or completely stop
in the case of a batch run.
@param mac= to contain the name of the calling macro
@param msg= message to be returned
@param iftrue= supply a condition under which the macro should be executed.
@version 9.4M3
@author Allan Bowe
**/
%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1)
)/*/STORE SOURCE*/;
%if not(%eval(%unquote(&iftrue))) %then %return;
%put NOTE: /// mp_abort macro executing //;
%if %length(&mac)>0 %then %put NOTE- called by &mac;
%put NOTE - &msg;
/* Stored Process Server web app context */
%if %symexist(_metaperson)
or (%symexist(SYSPROCESSNAME) and "&SYSPROCESSNAME"="Compute Server" )
%then %do;
options obs=max replace nosyntaxcheck mprint;
/* extract log errs / warns, if exist */
%local logloc logline;
%global logmsg; /* capture global messages */
%if %symexist(SYSPRINTTOLOG) %then %let logloc=&SYSPRINTTOLOG;
%else %let logloc=%qsysfunc(getoption(LOG));
proc printto log=log;run;
%if %length(&logloc)>0 %then %do;
%let logline=0;
data _null_;
infile &logloc lrecl=5000;
input; putlog _infile_;
i=1;
retain logonce 0;
if (_infile_=:"%str(WARN)ING" or _infile_=:"%str(ERR)OR") and logonce=0 then do;
call symputx('logline',_n_);
logonce+1;
end;
run;
/* capture log including lines BEFORE the err */
%if &logline>0 %then %do;
data _null_;
infile &logloc lrecl=5000;
input;
i=1;
stoploop=0;
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
input;
i+1;
stoploop=1;
end;
if stoploop=1 then stop;
run;
%end;
%end;
%if %symexist(SYS_JES_JOB_URI) %then %do;
/* refer web service output to file service in one hit */
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name="_webout.json";
%end;
/* send response in SASjs JSON format */
data _null_;
file _webout mod lrecl=32000;
length msg $32767 debug $8;
sasdatetime=datetime();
msg=cats(symget('msg'),'\n\nLog Extract:\n',symget('logmsg'));
/* escape the quotes */
msg=tranwrd(msg,'"','\"');
/* ditch the CRLFs as chrome complains */
msg=compress(msg,,'kw');
/* quote without quoting the quotes (which are escaped instead) */
msg=cats('"',msg,'"');
if symexist('_debug') then debug=quote(trim(symget('_debug')));
else debug='""';
if debug ge '"131"' then put '>>weboutBEGIN<<';
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
put ',"sasjsAbort" : [{';
put ' "MSG":' msg ;
put ' ,"MAC": "' "&mac" '"}]';
put ",""SYSUSERID"" : ""&sysuserid"" ";
put ',"_DEBUG":' debug ;
if symexist('_metauser') then do;
_METAUSER=quote(trim(symget('_METAUSER')));
put ",""_METAUSER"": " _METAUSER;
_METAPERSON=quote(trim(symget('_METAPERSON')));
put ',"_METAPERSON": ' _METAPERSON;
end;
if symexist('SYS_JES_JOB_URI') then do;
SYS_JES_JOB_URI=quote(trim(symget('SYS_JES_JOB_URI')));
put ',"SYS_JES_JOB_URI": ' SYS_JES_JOB_URI;
end;
_PROGRAM=quote(trim(resolve(symget('_PROGRAM'))));
put ',"_PROGRAM" : ' _PROGRAM ;
put ",""SYSCC"" : ""&syscc"" ";
put ",""SYSERRORTEXT"" : ""&syserrortext"" ";
put ",""SYSJOBID"" : ""&sysjobid"" ";
put ",""SYSWARNINGTEXT"" : ""&syswarningtext"" ";
put ',"END_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '" ';
put "}" @;
if debug ge '"131"' then put '>>weboutEND<<';
run;
%let syscc=0;
%if %symexist(_metaport) %then %do;
data _null_;
if symexist('sysprocessmode')
then if symget("sysprocessmode")="SAS Stored Process Server"
then rc=stpsrvset('program error', 0);
run;
%end;
/**
* endsas is reliable but kills some deployments.
* Abort variants are ungraceful (non zero return code)
* This approach lets SAS run silently until the end :-)
*/
%put _all_;
filename skip temp;
data _null_;
file skip;
put '%macro skip(); %macro skippy();';
run;
%inc skip;
%end;
%else %do;
%put _all_;
%abort cancel;
%end;
%mend;

55
base/mp_binarycopy.sas Executable file
View File

@@ -0,0 +1,55 @@
/**
@file
@brief Copy any file using binary input / output streams
@details Reads in a file byte by byte and writes it back out. Is an
os-independent method to copy files. In case of naming collision, the
default filerefs can be modified.
Based on http://stackoverflow.com/questions/13046116/using-sas-to-copy-a-text-file
%mp_binarycopy(inloc="/home/me/blah.txt", outref=_webout)
@param inloc full, quoted "path/and/filename.ext" of the object to be copied
@param outloc full, quoted "path/and/filename.ext" of object to be created
@param inref can override default input fileref to avoid naming clash
@param outref an override default output fileref to avoid naming clash
@returns nothing
@version 9.2
**/
%macro mp_binarycopy(
inloc= /* full path and filename of the object to be copied */
,outloc= /* full path and filename of object to be created */
,inref=____in /* override default to use own filerefs */
,outref=____out /* override default to use own filerefs */
)/*/STORE SOURCE*/;
/* these IN and OUT filerefs can point to anything */
%if &inref = ____in %then %do;
filename &inref &inloc lrecl=1048576 ;
%end;
%if &outref=____out %then %do;
filename &outref &outloc lrecl=1048576 ;
%end;
/* copy the file byte-for-byte */
data _null_;
length filein 8 fileid 8;
filein = fopen("&inref",'I',1,'B');
fileid = fopen("&outref",'O',1,'B');
rec = '20'x;
do while(fread(filein)=0);
rc = fget(filein,rec,1);
rc = fput(fileid, rec);
rc =fwrite(fileid);
end;
rc = fclose(filein);
rc = fclose(fileid);
run;
%if &inref = ____in %then %do;
filename &inref clear;
%end;
%if &outref=____out %then %do;
filename &outref clear;
%end;
%mend;

68
base/mp_cleancsv.sas Normal file
View File

@@ -0,0 +1,68 @@
/**
@file mp_cleancsv.sas
@brief Fixes embedded cr / lf / crlf in CSV
@details CSVs will sometimes contain lf or crlf within quotes (eg when
saved by excel). When the termstr is ALSO lf or crlf that can be tricky
to process using SAS defaults.
This macro converts any csv to follow the convention of a windows excel file,
applying CRLF line endings and converting embedded cr and crlf to lf.
usage:
fileref mycsv "/path/your/csv";
%mp_cleancsv(in=mycsv,out=/path/new.csv)
@param in= provide path or fileref to input csv
@param out= output path or fileref to output csv
@param qchar= quote char - hex code 22 is the double quote.
@version 9.2
@author Allan Bowe
**/
%macro mp_cleancsv(in=NOTPROVIDED,out=NOTPROVIDED,qchar='22'x);
%if "&in"="NOTPROVIDED" or "&out"="NOTPROVIDED" %then %do;
%put %str(ERR)OR: Please provide valid input (&in) and output (&out) locations;
%return;
%end;
/* presence of a period(.) indicates a physical location */
%if %index(&in,.) %then %let in="&in";
%if %index(&out,.) %then %let out="&out";
/**
* convert all cr and crlf within quotes to lf
* convert all other cr or lf to crlf
*/
data _null_;
infile &in recfm=n ;
file &out recfm=n;
retain isq iscrlf 0 qchar &qchar;
input inchar $char1. ;
if inchar=qchar then isq = mod(isq+1,2);
if isq then do;
/* inside a quote change cr and crlf to lf */
if inchar='0D'x then do;
put '0A'x;
input inchar $char1.;
if inchar ne '0A'x then do;
put inchar $char1.;
if inchar=qchar then isq = mod(isq+1,2);
end;
end;
else put inchar $char1.;
end;
else do;
/* outside a quote, change cr and lf to crlf */
if inchar='0D'x then do;
put '0D0A'x;
input inchar $char1.;
if inchar ne '0A'x then do;
put inchar $char1.;
if inchar=qchar then isq = mod(isq+1,2);
end;
end;
else if inchar='0A'x then put '0D0A'x;
else put inchar $char1.;
end;
run;
%mend;

View File

@@ -0,0 +1,67 @@
/**
@file mp_createconstraints.sas
@brief Creates constraints
@details Takes the output from mp_getconstraints.sas as input
proc sql;
create table work.example(
TX_FROM float format=datetime19.,
DD_TYPE char(16),
DD_SOURCE char(2048),
DD_SHORTDESC char(256),
constraint pk primary key(tx_from, dd_type,dd_source),
constraint unq unique(tx_from, dd_type),
constraint nnn not null(DD_SHORTDESC)
);
%mp_getconstraints(lib=work,ds=example,outds=work.constraints)
%mp_deleteconstraints(inds=work.constraints,outds=dropped,execute=YES)
%mp_createconstraints(inds=work.constraints,outds=created,execute=YES)
@param inds= The input table containing the constraint info
@param outds= a table containing the create statements (create_statement column)
@param execute= `YES|NO` - default is NO. To actually create, use YES.
<h4> Dependencies </h4>
@version 9.2
@author Allan Bowe
**/
%macro mp_createconstraints(inds=mp_getconstraints
,outds=mp_createconstraints
,execute=NO
)/*/STORE SOURCE*/;
proc sort data=&inds out=&outds;
by libref table_name constraint_name;
run;
data &outds;
set &outds;
by libref table_name constraint_name;
length create_statement $500;
if _n_=1 and "&execute"="YES" then call execute('proc sql;');
if first.constraint_name then do;
if constraint_type='PRIMARY' then type='PRIMARY KEY';
else type=constraint_type;
create_statement=catx(" ","alter table",libref,".",table_name
,"add constraint",constraint_name,type,"(");
if last.constraint_name then
create_statement=cats(create_statement,column_name,");");
else create_statement=cats(create_statement,column_name,",");
if "&execute"="YES" then call execute(create_statement);
end;
else if last.constraint_name then do;
create_statement=cats(column_name,");");
if "&execute"="YES" then call execute(create_statement);
end;
else do;
create_statement=cats(column_name,",");
if "&execute"="YES" then call execute(create_statement);
end;
output;
run;
%mend;

View File

@@ -0,0 +1,83 @@
/**
@file mp_createwebservice.sas
@brief Create a web service in SAS 9 or Viya
@details Creates a SASJS ready Stored Process in SAS 9 or Job Execution
Service in SAS Viya
Usage:
%* compile macros ;
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
%* write some code;
filename ft15f001 temp;
parmcards4;
%* fetch any data from frontend ;
%webout(FETCH)
data example1 example2;
set sashelp.class;
run;
%* send data back;
%webout(OPEN)
%webout(ARR,example1) * Array format, fast, suitable for large tables ;
%webout(OBJ,example2) * Object format, easier to work with ;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=appInit,code=ft15f001,replace=YES)
<h4> Dependencies </h4>
@li mf_getplatform.sas
@li mm_createwebservice.sas
@li mv_createwebservice.sas
@param path= The full folder path where the service will be created
@param name= Service name. Avoid spaces.
@param desc= The description of the service (optional)
@param precode= Space separated list of filerefs, pointing to the code that
needs to be attached to the beginning of the service (optional)
@param code= Space seperated fileref(s) of the actual code to be added
@param replace= select YES to replace any existing service in that location
@version 9.2
@author Allan Bowe
**/
%macro mp_createwebservice(path=HOME
,name=initService
,precode=
,code=ft15f001
,desc=This service was created by the mp_createwebservice macro
,replace=YES
)/*/STORE SOURCE*/;
%if &syscc ge 4 %then %do;
%put syscc=&syscc - &sysmacroname will not execute in this state;
%return;
%end;
%local platform; %let platform=%mf_getplatform();
%if &platform=SASVIYA %then %do;
%if "&path"="HOME" %then %let path=/Users/&sysuserid/My Folder;
%mv_createwebservice(path=&path
,name=&name
,code=&code
,precode=&precode
,desc=&desc
,replace=&replace
)
%end;
%else %do;
%if "&path"="HOME" %then %let path=/User Folders/&sysuserid/My Folder;
%mm_createwebservice(path=&path
,name=&name
,code=&code
,precode=&precode
,desc=&desc
,replace=&replace
)
%end;
%mend;

View File

@@ -0,0 +1,52 @@
/**
@file mp_deleteconstraints.sas
@brief Delete constraionts
@details Takes the output from mp_getconstraints.sas as input
proc sql;
create table work.example(
TX_FROM float format=datetime19.,
DD_TYPE char(16),
DD_SOURCE char(2048),
DD_SHORTDESC char(256),
constraint pk primary key(tx_from, dd_type,dd_source),
constraint unq unique(tx_from, dd_type),
constraint nnn not null(DD_SHORTDESC)
);
%mp_getconstraints(lib=work,ds=example,outds=work.constraints)
%mp_deleteconstraints(inds=work.constraints,outds=dropped,execute=YES)
@param inds= The input table containing the constraint info
@param outds= a table containing the drop statements (drop_statement column)
@param execute= `YES|NO` - default is NO. To actually drop, use YES.
@version 9.2
@author Allan Bowe
**/
%macro mp_deleteconstraints(inds=mp_getconstraints
,outds=mp_deleteconstraints
,execute=NO
)/*/STORE SOURCE*/;
proc sort data=&inds out=&outds;
by libref table_name constraint_name;
run;
data &outds;
set &outds;
by libref table_name constraint_name;
length drop_statement $500;
if _n_=1 and "&execute"="YES" then call execute('proc sql;');
if first.constraint_name then do;
drop_statement=catx(" ","alter table",libref,".",table_name
,"drop constraint",constraint_name,";");
output;
if "&execute"="YES" then call execute(drop_statement);
end;
run;
%mend;

155
base/mp_dirlist.sas Normal file
View File

@@ -0,0 +1,155 @@
/**
@file
@brief Returns all files and subdirectories within a specified parent
@details When used with getattrs=NO, is not OS specific (uses dopen / dread).
If getattrs=YES then the doptname / foptname functions are used to scan all
properties - any characters that are not valid in a SAS name (v7) are simply
stripped, and the table is transposed so theat each property is a column
and there is one file per row. An attempt is made to get all properties
whether a file or folder, but some files/folders cannot be accessed, and so
not all properties can / will be populated.
Credit for the rename approach:
https://communities.sas.com/t5/SAS-Programming/SAS-Function-to-convert-string-to-Legal-SAS-Name/m-p/27375/highlight/true#M5003
usage:
%mp_dirlist(path=/some/location,outds=myTable)
%mp_dirlist(outds=cwdfileprops, getattrs=YES)
@warning In a Unix environment, the existence of a named pipe will cause this
macro to hang. Therefore this tool should be used with caution in a SAS 9 web
application, as it can use up all available multibridge sessions if requests
are resubmitted.
If anyone finds a way to positively identify a named pipe using SAS (without
X CMD) do please raise an issue!
@param path= for which to return contents
@param outds= the output dataset to create
@param getattrs= YES/NO (default=NO). Uses doptname and foptname to return
all attributes for each file / folder.
@returns outds contains the following variables:
- file_or_folder (file / folder)
- filepath (path/to/file.name)
- filename (just the file name)
- ext (.extension)
- msg (system message if any issues)
- OS SPECIFIC variables, if <code>getattrs=</code> is used.
@version 9.2
@author Allan Bowe
**/
%macro mp_dirlist(path=%sysfunc(pathname(work))
, outds=work.mp_dirlist
, getattrs=NO
)/*/STORE SOURCE*/;
%let getattrs=%upcase(&getattrs)XX;
data &outds (compress=no keep=file_or_folder filepath filename ext msg);
length filepath $500 fref fref2 $8 file_or_folder $6 filename $80 ext $20 msg $200;
rc = filename(fref, "&path");
if rc = 0 then do;
did = dopen(fref);
if did=0 then do;
putlog "NOTE: This directory is empty - &path";
msg=sysmsg();
put _all_;
stop;
end;
rc = filename(fref);
end;
else do;
msg=sysmsg();
put _all_;
stop;
end;
dnum = dnum(did);
do i = 1 to dnum;
filename = dread(did, i);
rc = filename(fref2, "&path/"!!filename);
midd=dopen(fref2);
dmsg=sysmsg();
if did > 0 then file_or_folder='folder';
rc=dclose(midd);
midf=fopen(fref2);
fmsg=sysmsg();
if midf > 0 then file_or_folder='file';
rc=fclose(midf);
if index(fmsg,'File is in use') or index(dmsg,'is not a directory')
then file_or_folder='file';
else if index(fmsg, 'Insufficient authorization') then file_or_folder='file';
else if file_or_folder='' then file_or_folder='locked';
if file_or_folder='file' then do;
ext = prxchange('s/.*\.{1,1}(.*)/$1/', 1, filename);
if filename = ext then ext = ' ';
end;
else do;
ext='';
file_or_folder='folder';
end;
filepath="&path/"!!filename;
output;
end;
rc = dclose(did);
stop;
run;
%if %substr(&getattrs,1,1)=Y %then %do;
data &outds;
set &outds;
length infoname infoval $60 fref $8;
rc=filename(fref,filepath);
drop rc infoname fid i close fref;
if file_or_folder='file' then do;
fid=fopen(fref);
if fid le 0 then do;
msg=sysmsg();
putlog "Could not open file:" filepath fid= ;
sasname='_MCNOTVALID_';
output;
end;
else do i=1 to foptnum(fid);
infoname=foptname(fid,i);
infoval=finfo(fid,infoname);
sasname=compress(infoname, '_', 'adik');
if anydigit(sasname)=1 then sasname=substr(sasname,anyalpha(sasname));
if upcase(sasname) ne 'FILENAME' then output;
end;
close=fclose(fid);
end;
else do;
fid=dopen(fref);
if fid le 0 then do;
msg=sysmsg();
putlog "Could not open folder:" filepath fid= ;
sasname='_MCNOTVALID_';
output;
end;
else do i=1 to doptnum(fid);
infoname=doptname(fid,i);
infoval=dinfo(fid,infoname);
sasname=compress(infoname, '_', 'adik');
if anydigit(sasname)=1 then sasname=substr(sasname,anyalpha(sasname));
if upcase(sasname) ne 'FILENAME' then output;
end;
close=dclose(fid);
end;
run;
proc sort;
by filepath sasname;
proc transpose data=&outds out=&outds(drop=_:);
id sasname;
var infoval;
by filepath file_or_folder filename ext ;
run;
%end;
%mend;

View File

@@ -0,0 +1,50 @@
/**
@file
@brief Creates a dataset containing distinct _formatted_ values
@details If no format is supplied, then the original value is used instead.
There is also a dependency on other macros within the Macro Core library.
Usage:
%mp_distinctfmtvalues(libds=sashelp.class,var=age,outvar=age,outds=test)
@param libds input dataset
@param var variable to get distinct values for
@param outvar variable to create. Default: `formatted_value`
@param outds dataset to create. Default: work.mp_distinctfmtvalues
@param varlen length of variable to create (default 200)
@version 9.2
@author Allan Bowe
**/
%macro mp_distinctfmtvalues(
libds=
,var=
,outvar=formatted_value
,outds=work.mp_distinctfmtvalues
,varlen=2000
)/*/STORE SOURCE*/;
%local fmt vtype;
%let fmt=%mf_getvarformat(&libds,&var);
%let vtype=%mf_getvartype(&libds,&var);
proc sql;
create table &outds as
select distinct
%if &vtype=C & %trim(&fmt)=%str() %then %do;
&var
%end;
%else %if &vtype=C %then %do;
put(&var,&fmt)
%end;
%else %if %trim(&fmt)=%str() %then %do;
put(&var,32.)
%end;
%else %do;
put(&var,&fmt)
%end;
as &outvar length=&varlen
from &libds;
%mend;

38
base/mp_dropmembers.sas Executable file
View File

@@ -0,0 +1,38 @@
/**
@file
@brief Drops tables / views (if they exist) without warnings in the log
@details
Example usage:
proc sql;
create table data1 as select * from sashelp.class;
create view view2 as select * from sashelp.class;
%mp_dropmembers(list=data1 view2)
<h4> Dependencies </h4>
@li mf_isblank.sas
@param list space separated list of datasets / views
@param libref= can only drop from a single library at a time
@version 9.2
@author Allan Bowe
**/
%macro mp_dropmembers(
list /* space separated list of datasets / views */
,libref=WORK /* can only drop from a single library at a time */
)/*/STORE SOURCE*/;
%if %mf_isblank(&list) %then %do;
%put NOTE: nothing to drop!;
%return;
%end;
proc datasets lib=&libref nolist;
delete &list;
delete &list /mtype=view;
run;
%mend;

246
base/mp_ds2cards.sas Normal file
View File

@@ -0,0 +1,246 @@
/**
@file
@brief Create a CARDS file from a SAS dataset.
@details Uses dataset attributes to convert all data into datalines.
Running the generated file will rebuild the original dataset.
usage:
%mp_ds2cards(base_ds=sashelp.class
, cards_file= "C:\temp\class.sas"
, maxobs=5)
stuff to add
- labelling the dataset
- explicity setting a unix LF
- constraints / indexes etc
@param base_ds= Should be two level - eg work.blah. This is the table that
is converted to a cards file.
@param tgt_ds= Table that the generated cards file would create. Optional -
if omitted, will be same as BASE_DS.
@param cards_file= Location in which to write the (.sas) cards file
@param maxobs= to limit output to the first <code>maxobs</code> observations
@param showlog= whether to show generated cards file in the SAS log (YES/NO)
@param outencoding= provide encoding value for file statement (eg utf-8)
@version 9.2
@author Allan Bowe
**/
%macro mp_ds2cards(base_ds=, tgt_ds=
,cards_file="%sysfunc(pathname(work))/cardgen.sas"
,maxobs=max
,random_sample=NO
,showlog=YES
,outencoding=
)/*/STORE SOURCE*/;
%local i setds nvars;
%if not %sysfunc(exist(&base_ds)) %then %do;
%put WARNING: &base_ds does not exist;
%return;
%end;
%if %index(&base_ds,.)=0 %then %let base_ds=WORK.&base_ds;
%if (&tgt_ds = ) %then %let tgt_ds=&base_ds;
%if %index(&tgt_ds,.)=0 %then %let tgt_ds=WORK.%scan(&base_ds,2,.);
%if ("&outencoding" ne "") %then %let outencoding=encoding="&outencoding";
/* get varcount */
%let nvars=0;
proc sql noprint;
select count(*) into: nvars from dictionary.columns
where libname="%scan(%upcase(&base_ds),1)"
and memname="%scan(%upcase(&base_ds),2)";
%if &nvars=0 %then %do;
%put WARNING: Dataset &base_ds has no variables! It will not be converted.;
%return;
%end;
/* get indexes */
proc sort data=sashelp.vindex
(where=(upcase(libname)="%scan(%upcase(&base_ds),1)"
and upcase(memname)="%scan(%upcase(&base_ds),2)"))
out=_data_;
by indxname indxpos;
run;
%local indexes;
data _null_;
set &syslast end=last;
if _n_=1 then call symputx('indexes','(index=(','l');
by indxname indxpos;
length vars $32767 nom uni $8;
retain vars;
if first.indxname then do;
idxcnt+1;
nom='';
uni='';
vars=name;
end;
else vars=catx(' ',vars,name);
if last.indxname then do;
if nomiss='yes' then nom='/nomiss';
if unique='yes' then uni='/unique';
call symputx('indexes'
,catx(' ',symget('indexes'),indxname,'=(',vars,')',nom,uni)
,'l');
end;
if last then call symputx('indexes',cats(symget('indexes'),'))'),'l');
run;
data;run;
%let setds=&syslast;
proc sql
%if %datatyp(&maxobs)=NUMERIC %then %do;
outobs=&maxobs;
%end;
;
create table &setds as select * from &base_ds
%if &random_sample=YES %then %do;
order by ranuni(42)
%end;
;
create table datalines1 as
select name,type,length,varnum,format,label from dictionary.columns
where libname="%upcase(%scan(&base_ds,1))"
and memname="%upcase(%scan(&base_ds,2))";
/**
Due to long decimals cannot use best. format
So - use bestd. format and then use character functions to strip trailing
zeros, if NOT an integer!!
resolved code = ifc(int(VARIABLE)=VARIABLE
,put(VARIABLE,best32.)
,substrn(put(VARIABLE,bestd32.),1
,findc(put(VARIABLE,bestd32.),'0','TBK')));
**/
data datalines_2;
format dataline $32000.;
set datalines1 (where=(upcase(name) not in
('PROCESSED_DTTM','VALID_FROM_DTTM','VALID_TO_DTTM')));
if type='num' then dataline=
cats('ifc(int(',name,')=',name,'
,put(',name,',best32.-l)
,substrn(put(',name,',bestd32.-l),1
,findc(put(',name,',bestd32.-l),"0","TBK")))');
else dataline=name;
run;
proc sql noprint;
select dataline into: datalines separated by ',' from datalines_2;
%local
process_dttm_flg
valid_from_dttm_flg
valid_to_dttm_flg
;
%let process_dttm_flg = N;
%let valid_from_dttm_flg = N;
%let valid_to_dttm_flg = N;
data _null_;
set datalines1 ;
/* build attrib statement */
if type='char' then type2='$';
if strip(format) ne '' then format2=cats('format=',format);
if strip(label) ne '' then label2=cats('label=',quote(trim(label)));
str1=catx(' ',(put(name,$33.)||'length=')
,put(cats(type2,length),$7.)||format2,label2);
/* Build input statement */
if type='char' then type3=':$char.';
str2=put(name,$33.)||type3;
if(upcase(name) = "PROCESSED_DTTM") then
call symputx("process_dttm_flg", "Y", "L");
if(upcase(name) = "VALID_FROM_DTTM") then
call symputx("valid_from_dttm_flg", "Y", "L");
if(upcase(name) = "VALID_TO_DTTM") then
call symputx("valid_to_dttm_flg", "Y", "L");
call symputx(cats("attrib_stmt_", put(_N_, 8.)), str1, "L");
call symputx(cats("input_stmt_", put(_N_, 8.))
, ifc(upcase(name) not in
('PROCESSED_DTTM','VALID_FROM_DTTM','VALID_TO_DTTM'), str2, ""), "L");
run;
data _null_;
file &cards_file. &outencoding lrecl=32767 termstr=nl;
length __attrib $32767;
if _n_=1 then do;
put '/*******************************************************************';
put " Datalines for %upcase(%scan(&base_ds,2)) dataset ";
put " Generated by %nrstr(%%)mp_ds2cards()";
put " Available on github.com/macropeople/macrocore";
put '********************************************************************/';
put "data &tgt_ds &indexes;";
put "attrib ";
%do i = 1 %to &nvars;
__attrib=symget("attrib_stmt_&i");
put __attrib;
%end;
put ";";
%if &process_dttm_flg. eq Y %then %do;
put 'retain PROCESSED_DTTM %sysfunc(datetime());';
%end;
%if &valid_from_dttm_flg. eq Y %then %do;
put 'retain VALID_FROM_DTTM &low_date;';
%end;
%if &valid_to_dttm_flg. eq Y %then %do;
put 'retain VALID_TO_DTTM &high_date;';
%end;
if __nobs=0 then do;
put 'call missing(of _all_);/* avoid uninitialised notes */';
put 'stop;';
put 'run;';
end;
else do;
put "infile cards dsd delimiter=',';";
put "input ";
%do i = 1 %to &nvars.;
%if(%length(&&input_stmt_&i..)) %then
put " &&input_stmt_&i..";
;
%end;
put ";";
put "datalines4;";
end;
end;
set &setds end=__lastobs nobs=__nobs;
/* remove all formats for write purposes - some have long underlying decimals */
format _numeric_ best30.29;
length __dataline $32767;
__dataline=catq('cqtmb',&datalines);
put __dataline;
if __lastobs then do;
put ';;;;';
put 'run;';
stop;
end;
run;
proc sql;
drop table &setds;
quit;
%if &showlog=YES %then %do;
data _null_;
infile &cards_file lrecl=32767;
input;
put _infile_;
run;
%end;
%put NOTE: CARDS FILE SAVED IN:;
%put NOTE-;%put NOTE-;
%put NOTE- %sysfunc(dequote(&cards_file.));
%put NOTE-;%put NOTE-;
%mend;

View File

@@ -0,0 +1,60 @@
/**
@file mp_getconstraints.sas
@brief Get constraint details at column level
@details Useful for capturing constraints before they are dropped / reapplied
during an update.
proc sql;
create table work.example(
TX_FROM float format=datetime19.,
DD_TYPE char(16),
DD_SOURCE char(2048),
DD_SHORTDESC char(256),
constraint pk primary key(tx_from, dd_type,dd_source),
constraint unq unique(tx_from, dd_type),
constraint nnn not null(DD_SHORTDESC)
);
%mp_getconstraints(lib=work,ds=example,outds=work.constraints)
@param lib= The target library (default=WORK)
@param ds= The target dataset. Leave blank (default) for all datasets.
@param outds the output dataset
<h4> Dependencies </h4>
@version 9.2
@author Allan Bowe
**/
%macro mp_getconstraints(lib=WORK
,ds=
,outds=mp_getconstraints
)/*/STORE SOURCE*/;
%let lib=%upcase(&lib);
%let ds=%upcase(&ds);
/* must use SQL as proc datasets does not support length changes */
proc sql noprint;
create table &outds as
select a.TABLE_CATALOG as libref
,a.TABLE_NAME
,a.constraint_type
,a.constraint_name
,b.column_name
from dictionary.TABLE_CONSTRAINTS a
left join dictionary.constraint_column_usage b
on a.TABLE_CATALOG=b.TABLE_CATALOG
and a.TABLE_NAME=b.TABLE_NAME
and a.constraint_name=b.constraint_name
where a.TABLE_CATALOG="&lib"
and b.TABLE_CATALOG="&lib"
%if "&ds" ne "" %then %do;
and a.TABLE_NAME="&ds"
and b.TABLE_NAME="&ds"
%end;
;
%mend;

230
base/mp_getddl.sas Normal file
View File

@@ -0,0 +1,230 @@
/**
@file mp_getddl.sas
@brief Extract DDL in various formats, by table or library
@details Data Definition Language relates to a set of SQL instructions used
to create tables in SAS or a database. The macro can be used at table or
library level. The default behaviour is to create DDL in SAS format.
Usage:
data test(index=(pk=(x y)/unique /nomiss));
x=1;
y='blah';
label x='blah';
run;
proc sql; describe table &syslast;
%mp_getddl(work,test,flavour=tsql,showlog=YES)
@param lib libref of the library to create DDL for. Should be assigned.
@param ds dataset to create ddl for
@param fref= the fileref to which to write the DDL. If not preassigned, will
be assigned to TEMP.
@param flavour= The type of DDL to create (default=SAS). Supported=TSQL
@param showlog= Set to YES to show the DDL in the log
@param schema= Choose a preferred schema name (default is to use actual schema
,else libref)
@param applydttm= for non SAS DDL, choose if columns are created with native
datetime2 format or regular decimal type
@version 9.3
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_getddl(libref,ds,fref=getddl,flavour=SAS,showlog=NO,schema=
,applydttm=NO
)/*/STORE SOURCE*/;
/* check fileref is assigned */
%if %sysfunc(fileref(&fref)) > 0 %then %do;
filename &fref temp;
%end;
%if %length(&libref)=0 %then %let libref=WORK;
%let flavour=%upcase(&flavour);
proc sql noprint;
create table _data_ as
select * from dictionary.tables
where upcase(libname)="%upcase(&libref)"
%if %length(&ds)>0 %then %do;
and upcase(memname)="%upcase(&ds)"
%end;
;
%local tabinfo; %let tabinfo=&syslast;
create table _data_ as
select * from dictionary.indexes
where upcase(libname)="%upcase(&libref)"
%if %length(&ds)>0 %then %do;
and upcase(memname)="%upcase(&ds)"
%end;
order by idxusage, indxname, indxpos
;
%local idxinfo; %let idxinfo=&syslast;
create table _data_ as
select * from dictionary.columns
where upcase(libname)="%upcase(&libref)"
%if %length(&ds)>0 %then %do;
and upcase(memname)="%upcase(&ds)"
%end;
;
%local colinfo; %let colinfo=&syslast;
%local dsnlist;
select distinct upcase(memname) into: dsnlist
separated by ' '
from &syslast;
data _null_;
file &fref;
put "/* DDL generated by &sysuserid on %sysfunc(datetime(),datetime19.) */";
run;
%local x curds;
%if &flavour=SAS %then %do;
data _null_;
file &fref;
put "proc sql;";
run;
%do x=1 %to %sysfunc(countw(&dsnlist));
%let curds=%scan(&dsnlist,&x);
data _null_;
file &fref mod;
length nm lab $1024;
set &colinfo (where=(upcase(memname)="&curds")) end=last;
if _n_=1 then do;
if memtype='DATA' then do;
put "create table &libref..&curds(";
end;
else do;
put "create view &libref..&curds(";
end;
put " "@@;
end;
else put " ,"@@;
if length(format)>1 then fmt=" format="!!cats(format);
len=" length="!!cats(length);
lab=" label="!!quote(trim(label));
if notnull='yes' then notnul=' not null';
put name type len fmt notnul lab;
if last then put ');';
run;
data _null_;
length ds $128;
set &idxinfo (where=(memname="&curds")) end=last;
file &fref mod;
by idxusage indxname;
if unique='yes' then uniq=' unique';
ds=cats(libname,'.',memname);
if first.indxname then do;
put 'create ' uniq ' index ' indxname;
put ' on ' ds '(' name @@;
end;
else put ',' name @@;
if last.indxname then put ');';
run;
/*
ods output IntegrityConstraints=ic;
proc contents data=testali out2=info;
run;
*/
%end;
%end;
%else %if &flavour=TSQL %then %do;
/* if schema does not exist, set to be same as libref */
%local schemaactual;
proc sql noprint;
select sysvalue into: schemaactual
from dictionary.libnames
where libname="&libref" and engine='SQLSVR';
%let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref));
%do x=1 %to %sysfunc(countw(&dsnlist));
%let curds=%scan(&dsnlist,&x);
data _null_;
file &fref mod;
put "/* DDL for &schema..&curds */";
data _null_;
file &fref mod;
set &colinfo (where=(upcase(memname)="&curds")) end=last;
if _n_=1 then do;
if memtype='DATA' then do;
put "create table [&schema].[&curds](";
end;
else do;
put "create view [&schema].[&curds](";
end;
put " "@@;
end;
else put " ,"@@;
format=upcase(format);
if 1=0 then; /* dummy if */
%if &applydttm=YES %then %do;
else if format=:'DATETIME' then fmt='[datetime2](7) ';
%end;
else if type='num' then fmt='[decimal](18,2)';
else if length le 8000 then fmt='[varchar]('!!cats(length)!!')';
else fmt=cats('[varchar](max)');
if notnull='yes' then notnul=' NOT NULL';
put name fmt notnul;
run;
data _null_;
length ds $128;
set &idxinfo (where=(memname="&curds"));
file &fref mod;
by idxusage indxname;
if unique='yes' then uniq=' unique';
ds=cats(libname,'.',memname);
if first.indxname then do;
if unique='yes' and nomiss='yes' then do;
put ' ,constraint [' indxname '] PRIMARY KEY';
end;
else if unique='yes' then do;
/* add nonclustered in case of multiple unique indexes */
put ' ,index [' indxname '] UNIQUE NONCLUSTERED';
end;
put ' (';
put ' [' name ']';
end;
else put ' ,[' name ']';
if last.indxname then do;
put ' )';
end;
run;
data _null_;
file &fref mod;
put ')';
put 'GO';
run;
/* add extended properties for labels */
data _null_;
file &fref mod;
length nm $64 lab $1024;
set &colinfo (where=(upcase(memname)="&curds" and label ne '')) end=last;
nm=cats("N'",tranwrd(name,"'","''"),"'");
lab=cats("N'",tranwrd(label,"'","''"),"'");
put ' ';
put "EXEC sys.sp_addextendedproperty ";
put " @name=N'MS_Description',@value=" lab ;
put " ,@level0type=N'SCHEMA',@level0name=N'&schema' ";
put " ,@level1type=N'TABLE',@level1name=N'&curds'";
put " ,@level2type=N'COLUMN',@level2name=" nm ;
if last then put 'GO';
run;
%end;
%end;
%if &showlog=YES %then %do;
options ps=max;
data _null_;
infile &fref;
input;
putlog _infile_;
run;
%end;
%mend;

72
base/mp_getmaxvarlengths.sas Executable file
View File

@@ -0,0 +1,72 @@
/**
@file mp_getmaxvarlengths.sas
@brief Scans a dataset to find the max length of the variable values
@details
This macro will scan a base dataset and produce an output dataset with two
columns:
- NAME Name of the base dataset column
- MAXLEN Maximum length of the data contained therein.
Character fields may be allocated very large widths (eg 32000) of which the maximum
value is likely to be much narrower. This macro was designed to enable a HTML
table to be appropriately sized however this could be used as part of a data
audit to ensure we aren't over-sizing our tables in relation to the data therein.
Numeric fields are converted using the relevant format to determine the width.
Usage:
%mp_getmaxvarlengths(sashelp.class,outds=work.myds)
@param libds Two part dataset (or view) reference.
@param outds= The output dataset to create
<h4> Dependencies </h4>
@li mf_getvarlist.sas
@li mf_getvartype.sas
@li mf_getvarformat.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_getmaxvarlengths(
libds /* libref.dataset to analyse */
,outds=work.mp_getmaxvarlengths /* name of output dataset to create */
)/*/STORE SOURCE*/;
%local vars x var fmt;
%let vars=%mf_getvarlist(libds=&libds);
proc sql;
create table &outds (rename=(
%do x=1 %to %sysfunc(countw(&vars,%str( )));
________&x=%scan(&vars,&x)
%end;
))
as select
%do x=1 %to %sysfunc(countw(&vars,%str( )));
%let var=%scan(&vars,&x);
%if &x>1 %then ,;
%if %mf_getvartype(&libds,&var)=C %then %do;
max(length(&var)) as ________&x
%end;
%else %do;
%let fmt=%mf_getvarformat(&libds,&var);
%put fmt=&fmt;
%if %str(&fmt)=%str() %then %do;
max(length(cats(&var))) as ________&x
%end;
%else %do;
max(length(put(&var,&fmt))) as ________&x
%end;
%end;
%end;
from &libds;
proc transpose data=&outds
out=&outds(rename=(_name_=NAME COL1=MAXLEN));
run;
%mend;

304
base/mp_guesspk.sas Normal file
View File

@@ -0,0 +1,304 @@
/**
@file mp_guesspk.sas
@brief Guess the primary key of a table
@details Tries to guess the primary key of a table based on the following logic:
* Columns with nulls are ignored
* Return only column combinations that provide unique results
* Start from one column, then move out to include composite keys of 2 to 6 columns
The library of the target should be assigned before using this macro.
Usage:
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
%mp_guesspk(sashelp.class,outds=classpks)
@param baseds The dataset to analyse
@param outds= The output dataset to contain the possible PKs
@param max_guesses= The total number of possible primary keys to generate. A
table is likely to have multiple unlikely PKs, so no need to list them all. Default=3.
@param min_rows= The minimum number of rows a table should have in order to try
and guess the PK. Default=5.
<h4> Dependencies </h4>
@li mf_getvarlist.sas
@li mf_getuniquename.sas
@li mf_nobs.sas
@version 9.3
@author Allan Bowe
**/
%macro mp_guesspk(baseds
,outds=mp_guesspk
,max_guesses=3
,min_rows=5
)/*/STORE SOURCE*/;
/* declare local vars */
%local var vars vcnt i j k l tmpvar tmpds rows posspks ppkcnt;
%let vars=%mf_getvarlist(&baseds);
%let vcnt=%sysfunc(countw(&vars));
%if &vcnt=0 %then %do;
%put &sysmacroname: &baseds has no variables! Exiting.;
%return;
%end;
/* get null count and row count */
%let tmpvar=%mf_getuniquename();
proc sql noprint;
create table _data_ as select
count(*) as &tmpvar
%do i=1 %to &vcnt;
%let var=%scan(&vars,&i);
,sum(case when &var is missing then 1 else 0 end) as &var
%end;
from &baseds;
/* transpose table and scan for not null cols */
proc transpose;
data _null_;
set &syslast end=last;
length vars $32767;
retain vars ;
if _name_="&tmpvar" then call symputx('rows',col1,'l');
else if col1=0 then vars=catx(' ',vars,_name_);
if last then call symputx('posspks',vars,'l');
run;
%let ppkcnt=%sysfunc(countw(&posspks));
%if &ppkcnt=0 %then %do;
%put &sysmacroname: &baseds has no non-missing variables! Exiting.;
%return;
%end;
proc sort data=&baseds(keep=&posspks) out=_data_ noduprec;
by _all_;
run;
%local pkds; %let pkds=&syslast;
%if &rows > %mf_nobs(&pkds) %then %do;
%put &sysmacroname: &baseds has no combination of unique records! Exiting.;
%return;
%end;
/* now check cardinality */
proc sql noprint;
create table _data_ as select
%do i=1 %to &ppkcnt;
%let var=%scan(&posspks,&i);
count(distinct &var) as &var
%if &i<&ppkcnt %then ,;
%end;
from &pkds;
/* transpose and sort by cardinality */
proc transpose;
proc sort; by descending col1;
run;
/* create initial PK list and re-order posspks list */
data &outds(keep=pkguesses);
length pkguesses $5000 vars $5000;
set &syslast end=last;
retain vars ;
vars=catx(' ',vars,_name_);
if col1=&rows then do;
pkguesses=_name_;
output;
end;
if last then call symputx('posspks',vars,'l');
run;
%if %mf_nobs(&outds) ge &max_guesses %then %do;
%put &sysmacroname: %mf_nobs(&outds) possible primary key values found;
%return;
%end;
%if &ppkcnt=1 %then %do;
%put &sysmacroname: No more PK guess possible;
%return;
%end;
/* begin scanning for uniques on pairs of PKs */
%let tmpds=%mf_getuniquename();
%local lev1 lev2;
%do i=1 %to &ppkcnt;
%let lev1=%scan(&posspks,&i);
%do j=2 %to &ppkcnt;
%let lev2=%scan(&posspks,&j);
%if &lev1 ne &lev2 %then %do;
/* check for two level uniqueness */
proc sort data=&pkds(keep=&lev1 &lev2) out=&tmpds noduprec;
by _all_;
run;
%if %mf_nobs(&tmpds)=&rows %then %do;
proc sql;
insert into &outds values("&lev1 &lev2");
%if %mf_nobs(&outds) ge &max_guesses %then %do;
%put &sysmacroname: Max PKs reached at Level 2 for &baseds;
%return;
%end;
%end;
%end;
%end;
%end;
%if &ppkcnt=2 %then %do;
%put &sysmacroname: No more PK guess possible;
%return;
%end;
/* begin scanning for uniques on PK triplets */
%local lev3;
%do i=1 %to &ppkcnt;
%let lev1=%scan(&posspks,&i);
%do j=2 %to &ppkcnt;
%let lev2=%scan(&posspks,&j);
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
%let lev3=%scan(&posspks,&k);
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do;
/* check for three level uniqueness */
proc sort data=&pkds(keep=&lev1 &lev2 &lev3) out=&tmpds noduprec;
by _all_;
run;
%if %mf_nobs(&tmpds)=&rows %then %do;
proc sql;
insert into &outds values("&lev1 &lev2 &lev3");
%if %mf_nobs(&outds) ge &max_guesses %then %do;
%put &sysmacroname: Max PKs reached at Level 3 for &baseds;
%return;
%end;
%end;
%end;
%end;
%end;
%end;
%if &ppkcnt=3 %then %do;
%put &sysmacroname: No more PK guess possible;
%return;
%end;
/* scan for uniques on up to 4 PK fields */
%local lev4;
%do i=1 %to &ppkcnt;
%let lev1=%scan(&posspks,&i);
%do j=2 %to &ppkcnt;
%let lev2=%scan(&posspks,&j);
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
%let lev3=%scan(&posspks,&k);
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt;
%let lev4=%scan(&posspks,&l);
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then %do;
/* check for four level uniqueness */
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4) out=&tmpds noduprec;
by _all_;
run;
%if %mf_nobs(&tmpds)=&rows %then %do;
proc sql;
insert into &outds values("&lev1 &lev2 &lev3 &lev4");
%if %mf_nobs(&outds) ge &max_guesses %then %do;
%put &sysmacroname: Max PKs reached at Level 4 for &baseds;
%return;
%end;
%end;
%end;
%end;
%end;
%end;
%end;
%if &ppkcnt=4 %then %do;
%put &sysmacroname: No more PK guess possible;
%return;
%end;
/* scan for uniques on up to 4 PK fields */
%local lev5 m;
%do i=1 %to &ppkcnt;
%let lev1=%scan(&posspks,&i);
%do j=2 %to &ppkcnt;
%let lev2=%scan(&posspks,&j);
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
%let lev3=%scan(&posspks,&k);
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt;
%let lev4=%scan(&posspks,&l);
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then
%do m=5 %to &ppkcnt;
%let lev5=%scan(&posspks,&m);
%if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5 %then %do;
/* check for four level uniqueness */
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5) out=&tmpds noduprec;
by _all_;
run;
%if %mf_nobs(&tmpds)=&rows %then %do;
proc sql;
insert into &outds values("&lev1 &lev2 &lev3 &lev4 &lev5");
%if %mf_nobs(&outds) ge &max_guesses %then %do;
%put &sysmacroname: Max PKs reached at Level 5 for &baseds;
%return;
%end;
%end;
%end;
%end;
%end;
%end;
%end;
%end;
%if &ppkcnt=5 %then %do;
%put &sysmacroname: No more PK guess possible;
%return;
%end;
/* scan for uniques on up to 4 PK fields */
%local lev6 n;
%do i=1 %to &ppkcnt;
%let lev1=%scan(&posspks,&i);
%do j=2 %to &ppkcnt;
%let lev2=%scan(&posspks,&j);
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
%let lev3=%scan(&posspks,&k);
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt;
%let lev4=%scan(&posspks,&l);
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then
%do m=5 %to &ppkcnt;
%let lev5=%scan(&posspks,&m);
%if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5 %then
%do n=6 %to &ppkcnt;
%let lev6=%scan(&posspks,&n);
%if &lev1 ne &lev6 & &lev2 ne &lev6 & &lev3 ne &lev6
& &lev4 ne &lev6 & &lev5 ne &lev6 %then
%do;
/* check for four level uniqueness */
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5 &lev6)
out=&tmpds noduprec;
by _all_;
run;
%if %mf_nobs(&tmpds)=&rows %then %do;
proc sql;
insert into &outds values("&lev1 &lev2 &lev3 &lev4 &lev5 &lev6");
%if %mf_nobs(&outds) ge &max_guesses %then %do;
%put &sysmacroname: Max PKs reached at Level 6 for &baseds;
%return;
%end;
%end;
%end;
%end;
%end;
%end;
%end;
%end;
%end;
%if &ppkcnt=6 %then %do;
%put &sysmacroname: No more PK guess possible;
%return;
%end;
%mend;

163
base/mp_jsonout.sas Normal file
View File

@@ -0,0 +1,163 @@
/**
@file mp_jsonout.sas
@brief Writes JSON in SASjs format to a fileref
@details PROC JSON is faster but will produce errs like the ones below if
special chars are encountered.
>An object or array close is not valid at this point in the JSON text.
>Date value out of range
If this happens, try running with ENGINE=DATASTEP.
Usage:
filename tmp temp;
data class; set sashelp.class;run;
%mp_jsonout(OBJ,class,jref=tmp)
data _null_;
infile tmp;
input;list;
run;
If you are building web apps with SAS then you are strongly encouraged to use
the mX_createwebservice macros in combination with [sasjs](https://github.com/macropeople/sasjs).
For more information see https://sasjs.io
@param action Valid values:
* OPEN - opens the JSON
* OBJ - sends a table with each row as an object
* ARR - sends a table with each row in an array
* CLOSE - closes the JSON
@param ds the dataset to send. Must be a work table.
@param jref= the fileref to which to send the JSON
@param dslabel= the name to give the table in the exported JSON
@param fmt= Whether to keep or strip formats from the table
@param engine= Which engine to use to send the JSON, options are:
* PROCJSON (default)
* DATASTEP
@param dbg= Typically used with an _debug (numeric) option
@version 9.2
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=PROCJSON,dbg=0
)/*/STORE SOURCE*/;
%put output location=&jref;
%if &action=OPEN %then %do;
data _null_;file &jref encoding='utf-8';
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
run;
%end;
%else %if (&action=ARR or &action=OBJ) %then %do;
options validvarname=upcase;
data _null_;file &jref mod encoding='utf-8';
put ", ""%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":";
%if &engine=PROCJSON %then %do;
data;run;%let tempds=&syslast;
proc sql;drop table &tempds;
data &tempds /view=&tempds;set &ds;
%if &fmt=N %then format _numeric_ best32.;;
proc json out=&jref
%if &action=ARR %then nokeys ;
%if &dbg ge 131 %then pretty ;
;export &tempds / nosastags fmtnumeric;
run;
proc sql;drop view &tempds;
%end;
%else %if &engine=DATASTEP %then %do;
%local cols i tempds;
%let cols=0;
%if %sysfunc(exist(&ds)) ne 1 & %sysfunc(exist(&ds,VIEW)) ne 1 %then %do;
%put &sysmacroname: &ds NOT FOUND!!!;
%return;
%end;
data _null_;file &jref mod ;
put "["; call symputx('cols',0,'l');
proc sort data=sashelp.vcolumn(where=(libname='WORK' & memname="%upcase(&ds)"))
out=_data_;
by varnum;
data _null_;
set _last_ end=last;
call symputx(cats('name',_n_),name,'l');
call symputx(cats('type',_n_),type,'l');
call symputx(cats('len',_n_),length,'l');
if last then call symputx('cols',_n_,'l');
run;
proc format; /* credit yabwon for special null removal */
value bart ._ - .z = null
other = [best.];
data;run; %let tempds=&syslast; /* temp table for spesh char management */
proc sql; drop table &tempds;
data &tempds/view=&tempds;
attrib _all_ label='';
%do i=1 %to &cols;
%if &&type&i=char %then %do;
length &&name&i $32767;
format &&name&i $32767.;
%end;
%end;
set &ds;
format _numeric_ bart.;
%do i=1 %to &cols;
%if &&type&i=char %then %do;
&&name&i='"'!!trim(prxchange('s/"/\"/',-1,
prxchange('s/'!!'0A'x!!'/\n/',-1,
prxchange('s/'!!'0D'x!!'/\r/',-1,
prxchange('s/'!!'09'x!!'/\t/',-1,
prxchange('s/\\/\\\\/',-1,&&name&i)
)))))!!'"';
%end;
%end;
run;
/* write to temp loc to avoid _webout truncation - https://support.sas.com/kb/49/325.html */
filename _sjs temp lrecl=131068 encoding='utf-8';
data _null_; file _sjs lrecl=131068 encoding='utf-8' mod;
set &tempds;
if _n_>1 then put "," @; put
%if &action=ARR %then "[" ; %else "{" ;
%do i=1 %to &cols;
%if &i>1 %then "," ;
%if &action=OBJ %then """&&name&i"":" ;
&&name&i
%end;
%if &action=ARR %then "]" ; %else "}" ; ;
proc sql;
drop view &tempds;
/* now write the long strings to _webout 1 byte at a time */
data _null_;
length filein 8 fileid 8;
filein = fopen("_sjs",'I',1,'B');
fileid = fopen("&jref",'A',1,'B');
rec = '20'x;
do while(fread(filein)=0);
rc = fget(filein,rec,1);
rc = fput(fileid, rec);
rc =fwrite(fileid);
end;
rc = fclose(filein);
rc = fclose(fileid);
run;
filename _sjs clear;
data _null_; file &jref mod encoding='utf-8';
put "]";
run;
%end;
%end;
%else %if &action=CLOSE %then %do;
data _null_;file &jref encoding='utf-8';
put "}";
run;
%end;
%mend;

51
base/mp_lib2cards.sas Normal file
View File

@@ -0,0 +1,51 @@
/**
@file
@brief Convert all library members to CARDS files
@details Gets list of members then calls the <code>%mp_ds2cards()</code>
macro
usage:
%mp_lib2cards(lib=sashelp
, outloc= C:\temp )
<h4> Dependencies </h4>
@li mf_mkdir.sas
@li mp_ds2cards.sas
@param lib= Library in which to convert all datasets
@param outloc= Location in which to store output. Defaults to WORK library.
Do not use a trailing slash (my/path not my/path/). No quotes.
@param maxobs= limit output to the first <code>maxobs</code> observations
@version 9.2
@author Allan Bowe
**/
%macro mp_lib2cards(lib=
,outloc=%sysfunc(pathname(work)) /* without trailing slash */
,maxobs=max
,random_sample=NO
)/*/STORE SOURCE*/;
/* Find the tables */
%local x ds memlist;
proc sql noprint;
select distinct lowcase(memname)
into: memlist
separated by ' '
from dictionary.tables
where upcase(libname)="%upcase(&lib)";
/* create the output directory */
%mf_mkdir(&outloc)
/* create the cards files */
%do x=1 %to %sysfunc(countw(&memlist));
%let ds=%scan(&memlist,&x);
%mp_ds2cards(base_ds=&lib..&ds
,cards_file="&outloc/&ds..sas"
,maxobs=&maxobs
,random_sample=&random_sample)
%end;
%mend;

42
base/mp_perflog.sas Normal file
View File

@@ -0,0 +1,42 @@
/**
@file
@brief Logs the time the macro was executed in a control dataset.
@details If the dataset does not exist, it is created. Usage:
%mp_perflog(started)
%mp_perflog()
%mp_perflog(startanew,libds=work.newdataset)
%mp_perflog(finished,libds=work.newdataset)
%mp_perflog(finished)
@param label Provide label to go into the control dataset
@param libds= Provide a dataset in which to store performance stats. Default
name is <code>work.mp_perflog</code>;
@version 9.2
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_perflog(label,libds=work.mp_perflog
)/*/STORE SOURCE*/;
%if not (%mf_existds(&libds)) %then %do;
data &libds;
length sysjobid $10 label $256 dttm 8.;
format dttm datetime19.3;
call missing(of _all_);
stop;
run;
%end;
proc sql;
insert into &libds
set sysjobid="&sysjobid"
,label=symget('label')
,dttm=%sysfunc(datetime());
quit;
%mend;

90
base/mp_recursivejoin.sas Normal file
View File

@@ -0,0 +1,90 @@
/**
@file
@brief Returns all children from a hierarchy table for a specified parent
@details Where data stores hierarchies in a simple parent / child mapping,
it is not always straightforward to extract all the children for a
particular parent. This problem is known as a recursive self join. This
macro will extract all the descendents for a parent.
Usage:
data have;
p=1;c=2;output;
p=2;c=3;output;
p=2;c=4;output;
p=3;c=5;output;
p=6;c=7;output;
p=8;c=9;output;
run;
%mp_recursivejoin(base_ds=have
,outds=want
,matchval=1
,parentvar=p
,childvar=c
)
@param base_ds= base table containing hierarchy (not modified)
@param outds= the output dataset to create with the generated hierarchy
@param matchval= the ultimate parent from which to filter
@param parentvar= name of the parent variable
@param childvar= name of the child variable (should be same type as parent)
@param mdebug= set to 1 to prevent temp tables being dropped
@returns outds contains the following variables:
- level (0 = top level)
- &parentvar
- &childvar (null if none found)
@version 9.2
@author Allan Bowe
**/
%macro mp_recursivejoin(base_ds=
,outds=
,matchval=
,parentvar=
,childvar=
,iter= /* reserved for internal / recursive use by the macro itself */
,maxiter=500 /* avoid infinite loop */
,mDebug=0);
%if &iter= %then %do;
proc sql;
create table &outds as
select 0 as level,&parentvar, &childvar
from &base_ds
where &parentvar=&matchval;
%if &sqlobs.=0 %then %do;
%put NOTE: &sysmacroname: No match for &parentvar=&matchval;
%return;
%end;
%let iter=1;
%end;
%else %if &iter>&maxiter %then %return;
proc sql;
create table _data_ as
select &iter as level
,curr.&childvar as &parentvar
,base_ds.&childvar as &childvar
from &outds curr
left join &base_ds base_ds
on curr.&childvar=base_ds.&parentvar
where curr.level=%eval(&iter.-1)
& curr.&childvar is not null;
%local append_ds; %let append_ds=&syslast;
%local obs; %let obs=&sqlobs;
insert into &outds select distinct * from &append_ds;
%if &mdebug=0 %then drop table &append_ds;;
%if &obs %then %do;
%mp_recursivejoin(iter=%eval(&iter.+1)
,outds=&outds,parentvar=&parentvar
,childvar=&childvar
,base_ds=&base_ds
)
%end;
%mend;

32
base/mp_resetoption.sas Normal file
View File

@@ -0,0 +1,32 @@
/**
@file
@brief Reset an option to original value
@details Inspired by the SAS Jedi - https://blogs.sas.com/content/sastraining/2012/08/14/jedi-sas-tricks-reset-sas-system-options/
Called as follows:
options obs=30;
%mp_resetoption(OBS)
@param option the option to reset
@version 9.2
@author Allan Bowe
**/
%macro mp_resetoption(option /* the option to reset */
)/*/STORE SOURCE*/;
data _null_;
length code $1500;
startup=getoption("&option",'startupvalue');
current=getoption("&option");
if startup ne current then do;
code =cat('OPTIONS ',getoption("&option",'keyword','startupvalue'),';');
putlog "NOTE: Resetting system option: " code ;
call execute(code );
end;
run;
%mend;

49
base/mp_runddl.sas Normal file
View File

@@ -0,0 +1,49 @@
/**
@file mp_runddl.sas
@brief An opinionated way to execute DDL files in SAS.
@details When delivering projects there should be seperation between the DDL
used to generate the tables and the sample data used to populate them.
This macro expects certain folder structure - eg:
rootlib
|-- LIBREF1
| |__ mytable.ddl
| |__ someothertable.ddl
|-- LIBREF2
| |__ table1.ddl
| |__ table2.ddl
|-- LIBREF3
|__ table3.ddl
|__ table4.ddl
Only files with the .ddl suffix are executed. The parent folder name is used
as the libref.
Files should NOT contain the `proc sql` statement - this is to prevent
statements being executed if there is an err condition.
Usage:
%mp_runddl(/some/rootlib) * execute all libs ;
%mp_runddl(/some/rootlib, inc=LIBREF1 LIBREF2) * include only these libs;
%mp_runddl(/some/rootlib, exc=LIBREF3) * same as above ;
@param path location of the DDL folder structure
@param inc= list of librefs to include
@param exc= list of librefs to exclude (takes precedence over inc=)
@version 9.3
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_runddl(path, inc=, exc=
)/*/STORE SOURCE*/;
%mend;

71
base/mp_searchcols.sas Normal file
View File

@@ -0,0 +1,71 @@
/**
@file mp_searchcols.sas
@brief Searches all columns in a library
@details
Scans a set of libraries and creates a dataset containing all source tables
containing one or more of a particular set of columns
Usage:
%mp_searchcols(libs=sashelp work, cols=name sex age)
@param libs=
@version 9.2
@author Allan Bowe
**/
%macro mp_searchcols(libs=sashelp
,cols=
,outds=mp_searchcols
)/*/STORE SOURCE*/;
%put &sysmacroname process began at %sysfunc(datetime(),datetime19.);
/* get the list of tables in the library */
proc sql;
create table _data_ as
select distinct upcase(libname) as libname
, upcase(memname) as memname
, upcase(name) as name
from dictionary.columns
%if %sysevalf(%superq(libs)=,boolean)=0 %then %do;
where upcase(libname) in ("IMPOSSIBLE",
%local x;
%do x=1 %to %sysfunc(countw(&libs));
"%upcase(%scan(&libs,&x))"
%end;
)
%end;
order by 1,2,3;
data &outds;
set &syslast;
length cols matchcols $32767;
cols=upcase(symget('cols'));
colcount=countw(cols);
by libname memname name;
if _n_=1 then do;
putlog "Searching libs: &libs";
putlog "Searching cols: " cols;
end;
if first.memname then do;
sumcols=0;
retain matchcols;
matchcols='';
end;
if findw(cols,name,,'spit') then do;
sumcols+1;
matchcols=cats(matchcols)!!' '!!cats(name);
end;
if last.memname then do;
if sumcols>0 then output;
if sumcols=colcount then putlog "Full Match: " libname memname;
end;
keep libname memname sumcols matchcols;
run;
proc sort; by descending sumcols memname libname; run;
%put &sysmacroname process finished at %sysfunc(datetime(),datetime19.);
%mend;

107
base/mp_searchdata.sas Normal file
View File

@@ -0,0 +1,107 @@
/**
@file
@brief Searches all data in a library
@details
Scans an entire library and creates a copy of any table
containing a specific string or numeric value. Only
matching records are written out.
If both a string and numval are provided, the string
will take precedence.
Usage:
%mp_searchdata(lib=sashelp, string=Jan)
%mp_searchdata(lib=sashelp, numval=1)
Outputs zero or more tables to an MPSEARCH library with specific records.
@param lib= the libref to search (should be already assigned)
@param ds= the dataset to search (leave blank to search entire library)
@param string= the string value to search
@param numval= the numeric value to search (must be exact)
@param outloc= the directory in which to create the output datasets with matching
rows. Will default to a subfolder in the WORK library.
@param outobs= set to a positive integer to restrict the number of observations
@param filter_text= add a (valid) filter clause to further filter the results
<h4> Dependencies </h4>
@li mf_getvarlist.sas
@li mf_getvartype.sas
@li mf_mkdir.sas
@li mf_nobs.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_searchdata(lib=sashelp
,ds=
,string= /* the query will use a contains (?) operator */
,numval= /* numeric must match exactly */
,outloc=%sysfunc(pathname(work))/mpsearch
,outobs=-1
,filter_text=%str(1=1)
)/*/STORE SOURCE*/;
%local table_list table table_num table colnum col start_tm vars type coltype;
%put process began at %sysfunc(datetime(),datetime19.);
%if &string = %then %let type=N;
%else %let type=C;
%mf_mkdir(&outloc)
libname mpsearch "&outloc";
/* get the list of tables in the library */
proc sql noprint;
select distinct memname into: table_list separated by ' '
from dictionary.tables
where upcase(libname)="%upcase(&lib)"
%if &ds ne %then %do;
and upcase(memname)=%upcase("&ds")
%end;
;
/* check that we have something to check */
proc sql
%if &outobs>-1 %then %do;
outobs=&outobs
%end;
;
%if %length(&table_list)=0 %then %put library &lib contains no tables!;
/* loop through each table */
%else %do table_num=1 %to %sysfunc(countw(&table_list,%str( )));
%let table=%scan(&table_list,&table_num,%str( ));
%let vars=%mf_getvarlist(&lib..&table);
%if %length(&vars)=0 %then %do;
%put NO COLUMNS IN &lib..&table! This will be skipped.;
%end;
%else %do;
/* build sql statement */
create table mpsearch.&table as select * from &lib..&table
where %unquote(&filter_text) and
(0
/* loop through columns */
%do colnum=1 %to %sysfunc(countw(&vars,%str( )));
%let col=%scan(&vars,&colnum,%str( ));
%let coltype=%mf_getvartype(&lib..&table,&col);
%if &type=C and &coltype=C %then %do;
/* if a char column, see if it contains the string */
or (&col ? "&string")
%end;
%else %if &type=N and &coltype=N %then %do;
/* if numeric match exactly */
or (&col = &numval)
%end;
%end;
);
%if %mf_nobs(mpsearch.&table)=0 %then %do;
drop table mpsearch.&table;
%end;
%end;
%end;
%put process finished at %sysfunc(datetime(),datetime19.);
%mend;

52
base/mp_setkeyvalue.sas Normal file
View File

@@ -0,0 +1,52 @@
/**
@file
@brief Logs a key value pair a control dataset
@details If the dataset does not exist, it is created. Usage:
%mp_setkeyvalue(someindex,22,type=N)
%mp_setkeyvalue(somenewindex,somevalue)
<h4> Dependencies </h4>
@li mf_existds.sas
@param key Provide a key on which to perform the lookup
@param value Provide a value
@param type= either C or N will populate valc and valn respectively. C is
default.
@param libds= define the target table to hold the parameters
@version 9.2
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_setkeyvalue(key,value,type=C,libds=work.mp_setkeyvalue
)/*/STORE SOURCE*/;
%if not (%mf_existds(&libds)) %then %do;
data &libds (index=(key/unique));
length key $32 valc $256 valn 8 type $1;
call missing(of _all_);
stop;
run;
%end;
proc sql;
delete from &libds
where key=symget('key');
insert into &libds
set key=symget('key')
%if &type=C %then %do;
,valc=symget('value')
,type='C'
%end;
%else %do;
,valn=symgetn('value')
,type='N'
%end;
;
quit;
%mend;

73
base/mp_stprequests.sas Normal file
View File

@@ -0,0 +1,73 @@
/**
@file
@brief Capture session start / finish times and request details
@details For details, see http://www.rawsas.com/2015/09/logging-of-stored-process-server.html.
Requires a base table in the following structure (name can be changed):
proc sql;
create table &libds(
request_dttm num not null format=datetime.
,status_cd char(4) not null
,_metaperson varchar(100) not null
,_program varchar(500)
,sysuserid varchar(50)
,sysjobid varchar(12)
,_sessionid varchar(50)
);
Called via STP init / term events (configurable in SMC) as follows:
%mp_stprequests(status_cd=INIT, libds=YOURLIB.DATASET )
@param status_cd= Use INIT for INIT and TERM for TERM events
@param libds= Location of base table (library.dataset). To minimise risk
of table locks, we HIGHLY recommend using a database (NOT a SAS dataset).
THE LIBRARY SHOULD BE ASSIGNED ALREADY - eg in autoexec or earlier in the
init program proper.
@version 9.2
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_stprequests(status_cd= /* $4 eg INIT or TERM */
,libds=somelib.stp_requests /* base table location */
)/*/STORE SOURCE*/;
/* set nosyntaxcheck so the code runs regardless */
%local etls_syntaxcheck;
%let etls_syntaxcheck=%sysfunc(getoption(syntaxcheck));
options nosyntaxcheck;
data ;
if 0 then set &libds;
request_dttm=datetime();
status_cd="&status_cd";
_METAPERSON="&_metaperson";
_PROGRAM="&_program";
SYSUSERID="&sysuserid";
SYSJOBID="&sysjobid";
%if not %symexist(_SESSIONID) %then %do;
/* session id is stored in the replay variable but needs to be extracted */
_replay=symget('_replay');
_replay=subpad(_replay,index(_replay,'_sessionid=')+11,length(_replay));
index=index(_replay,'&')-1;
if index=-1 then index=length(_replay);
_replay=substr(_replay,1,index);
_SESSIONID=_replay;
drop _replay index;
%end;
%else %do;
/* explicitly created sessions are automatically available */
_SESSIONID=symget('_SESSIONID');
%end;
output;
stop;
run;
proc append base=&libds data=&syslast nowarn;run;
options &etls_syntaxcheck;
%mend;

101
base/mp_streamfile.sas Normal file
View File

@@ -0,0 +1,101 @@
/**
@file mp_streamfile.sas
@brief Streams a file to _webout according to content type
@details Will set headers using appropriate functions (SAS 9 vs Viya) and send
content as a binary stream.
Usage:
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
%mp_streamfile(contenttype=csv,inloc=/some/where.txt,outname=myfile.txt)
<h4> Dependencies </h4>
@li mf_getplatform.sas
@li mp_binarycopy.sas
@param contenttype= Either TEXT, ZIP, CSV, EXCEL (default TEXT)
@param inloc= /path/to/file.ext to be sent
@param outname= the name of the file, as downloaded by the browser
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_streamfile(
contenttype=TEXT
,inloc=
,outname=
)/*/STORE SOURCE*/;
%let contentype=%upcase(&contenttype);
%local platform; %let platform=%mf_getplatform();
%if &contentype=ZIP %then %do;
%if &platform=SASMETA %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/zip');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
run;
%end;
%else %if &platform=SASVIYA %then %do;
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.zip'
contenttype='application/zip'
contentdisp="attachment; filename=&outname";
%end;
%end;
%else %if &contentype=EXCEL %then %do;
%if &platform=SASMETA %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/vnd.ms-excel');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
run;
%end;
%else %if &platform=SASVIYA %then %do;
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.xls'
contenttype='application/vnd.ms-excel'
contentdisp="attachment; filename=&outname";
%end;
%end;
%else %if &contentype=TEXT %then %do;
%if &platform=SASMETA %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/text');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
run;
%end;
%else %if &platform=SASVIYA %then %do;
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.txt'
contenttype='application/text'
contentdisp="attachment; filename=&outname";
%end;
%end;
%else %if &contentype=CSV %then %do;
%if &platform=SASMETA %then %do;
data _null_;
rc=stpsrv_header('Content-type','application/csv');
rc=stpsrv_header('Content-disposition',"attachment; filename=&outname");
run;
%end;
%else %if &platform=SASVIYA %then %do;
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.txt'
contenttype='application/csv'
contentdisp="attachment; filename=&outname";
%end;
%end;
%else %if &contentype=HTML %then %do;
%if &platform=SASVIYA %then %do;
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name="_webout.json"
contenttype="text/html";
%end;
%end;
%else %do;
%put %str(ERR)OR: Content Type &contenttype NOT SUPPORTED by &sysmacroname!;
%return;
%end;
%mp_binarycopy(inloc="&inloc",outref=_webout)
%mend;

66
base/mp_unzip.sas Normal file
View File

@@ -0,0 +1,66 @@
/**
@file mp_unzip.sas
@brief Unzips a zip file
@details Opens the zip file and copies all the contents to another directory.
It is not possible to retain permissions / timestamps, also the BOF marker
is lost so it cannot extract binary files.
Usage:
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
%mp_unzip(ziploc="/some/file.zip",outdir=/some/folder)
<h4> Dependencies </h4>
@li mf_mkdir.sas
@li mf_getuniquefileref.sas
@param ziploc= fileref or quoted full path to zip file ("/path/to/file.zip")
@param outdir= directory in which to write the outputs (created if non existant)
@version 9.4
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_unzip(
ziploc=
,outdir=%sysfunc(pathname(work))
)/*/STORE SOURCE*/;
%local fname1 fname2 fname3;
%let fname1=%mf_getuniquefileref();
%let fname2=%mf_getuniquefileref();
%let fname3=%mf_getuniquefileref();
filename &fname1 ZIP &ziploc; * Macro variable &datazip would be read from the file*;
/* Read the "members" (files) from the ZIP file */
data _data_(keep=memname isFolder);
length memname $200 isFolder 8;
fid=dopen("&fname1");
if fid=0 then stop;
memcount=dnum(fid);
do i=1 to memcount;
memname=dread(fid,i);
/* check for trailing / in folder name */
isFolder = (first(reverse(trim(memname)))='/');
output;
end;
rc=dclose(fid);
run;
filename &fname1 clear;
/* loop through each entry and either create the subfolder or extract member */
data _null_;
set &syslast;
if isFolder then call execute('%mf_mkdir(&outdir/'!!memname!!')');
else call execute('filename &fname2 zip &ziploc member='
!!quote(trim(memname))!!';filename &fname3 "&outdir/'
!!trim(memname)!!'" recfm=n;data _null_; rc=fcopy("&fname2","&fname3");run;'
!!'filename &fname2 clear; filename &fname3 clear;');
run;
%mend;

View File

@@ -0,0 +1,93 @@
/**
@file mp_updatevarlength.sas
@brief Change the length of a variable
@details The library is assumed to be assigned. Simple character updates
currently supported, numerics are more complicated and will follow.
data example;
a='1';
b='12';
c='123';
run;
%mp_updatevarlength(example,a,3)
%mp_updatevarlength(example,c,1)
proc sql;
describe table example;
@param libds the library.dataset to be modified
@param var The variable to modify
@param len The new length to apply
<h4> Dependencies </h4>
@li mf_existds.sas
@li mp_abort.sas
@li mf_existvar.sas
@li mf_getvarlen.sas
@li mf_getvartype.sas
@li mf_getnobs.sas
@li mp_createconstraints.sas
@li mp_getconstraints.sas
@li mp_deleteconstraints.sas
@version 9.2
@author Allan Bowe
**/
%macro mp_updatevarlength(libds,var,len
)/*/STORE SOURCE*/;
%if %index(&libds,.)=0 %then %let libds=WORK.&libds;
%mp_abort(iftrue=(%mf_existds(&libds)=0)
,mac=&sysmacroname
,msg=%str(Table &libds not found!)
)
%mp_abort(iftrue=(%mf_existvar(&libds,&var)=0)
,mac=&sysmacroname
,msg=%str(Variable &var not found on &libds!)
)
/* not possible to in-place modify a numeric length, to add later */
%mp_abort(iftrue=(%mf_getvartype(&libds,&var)=0)
,mac=&sysmacroname
,msg=%str(Only character resizings are currently supported)
)
%local oldlen;
%let oldlen=%mf_getvarlen(&libds,&var);
%if &oldlen=&len %then %do;
%put &sysmacroname: Old and new lengths (&len) match!;
%return;
%end;
%let libds=%upcase(&libds);
data;run;
%local dsconst; %let dsconst=&syslast;
%mp_getconstraints(lib=%scan(&libds,1,.),ds=%scan(&libds,2,.),outds=&dsconst)
%mp_abort(iftrue=(&syscc ne 0)
,mac=&sysmacroname
,msg=%str(syscc=&syscc)
)
%if %mf_getnobs(&dscont)=0 %then %do;
/* must use SQL as proc datasets does not support length changes */
proc sql;
alter table &libds modify &var char(&len);
%return;
%end;
/* we have constraints! */
%mp_deleteconstraints(inds=&dsconst,outds=&dsconst._dropd,execute=YES)
proc sql;
alter table &libds modify &var char(&len);
%mp_createconstraints(inds=&dsconst,outds=&dsconst._addd,execute=YES)
%mend;

79
base/mp_zip.sas Normal file
View File

@@ -0,0 +1,79 @@
/**
@file
@brief Creates a zip file
@details For DIRECTORY usage, will ignore subfolders. For DATASET usage,
provide a column that contains the full file path to each file to be zipped.
%mp_zip(in=myzips,type=directory,outname=myDir)
%mp_zip(in=/my/file/path.txt,type=FILE,outname=myFile)
%mp_zip(in=SOMEDS,incol=FPATH,type=DATASET,outname=myFile)
If you are sending zipped output to the _webout destination as part of an STP
be sure that _debug is not set (else the SPWA will send non zipped content
as well).
<h4> Dependencies </h4>
@li mp_dirlist.sas
@param in= unquoted filepath, dataset of files or directory to zip
@param type= FILE, DATASET, DIRECTORY. (FILE / DATASET not ready yet)
@param outname= output file to create, without .zip extension
@param outpath= location for output zip file
@param incol= if DATASET input, say which column contains the filepath
@version 9.2
@author Allan Bowe
@source https://github.com/macropeople/macrocore
**/
%macro mp_zip(
in=
,type=FILE
,outname=FILE
,outpath=%sysfunc(pathname(WORK))
,incol=
,debug=NO
)/*/STORE SOURCE*/;
%let type=%upcase(&type);
%local ds;
ods package open nopf;
%if &type=FILE %then %do;
ods package add file="&in" mimetype="application/x-compress";
%end;
%else %if &type=DIRECTORY %then %do;
%mp_dirlist(path=&in,outds=_data_)
%let ds=&syslast;
data _null_;
set &ds;
length __command $4000;
if file_or_folder='file';
command=cats('ods package add file="',filepath
,'" mimetype="application/x-compress";');
call execute(command);
run;
/* tidy up */
%if &debug=NO %then %do;
proc sql; drop table &ds;quit;
%end;
%end;
%else %if &type=DATASET %then %do;
data _null_;
set &in;
length __command $4000;
command=cats('ods package add file="',&incol
,'" mimetype="application/x-compress";');
call execute(command);
run;
ods package add file="&in" mimetype="application/x-compress";
%end;
ods package publish archive properties
(archive_name="&outname..zip" archive_path="&outpath");
ods package close;
%mend;