1
0
mirror of https://github.com/sasjs/core.git synced 2025-12-10 22:14:35 +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

21
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: SASjs Core Publish
on:
push:
branches:
- master
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

83
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,83 @@
# Contributing
Contributions to the macrocore library are warmly welcomed! To avoid any
misunderstandings, do please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before submitting
a PR.
Please note we have a code of conduct, please follow it in all your interactions
with the project.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at support@macropeople.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

2452
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2018 (Allan Bowe)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

108
README.md Normal file
View File

@@ -0,0 +1,108 @@
# Macro Core
Much quality. Many standards. The **Macro Core** library exists to save time and development effort! Herein ye shall find a veritable host of MIT-licenced, production quality SAS macros. These are a mix of tools, utilities, functions and code generators that are useful in the context of Application Development on the SAS platform (eg https://datacontroller.io). [Contributions](https://github.com/sasjs/core/blob/master/CONTRIBUTING.md) are welcomed.
You can download and compile them all in just two lines of SAS code:
```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/master/mc_all.sas";
%inc mc;
```
Documentation: https://sasjs.github.io/core.github.io/files.html
# Components
**base** library (SAS9/Viya)
- OS independent
- Not metadata aware
- No X command
- Prefixes: _mf_, _mp_
**meta** library (SAS9 only)
- OS independent
- Metadata aware
- No X command
- Prefixes: _mm_
**viya** library (Viya only)
- OS independent
- No X command
- Prefixes: _mv_
**metax** library (SAS9 only)
- OS specific
- Metadata aware
- X command enabled
- Prefixes: _mmw_,_mmu_,_mmx_
# Installation
First, download the repo to a location your SAS system can access. Then update your sasautos path to include the components you wish to have available,eg:
```sas
options insert=(sasautos="/your/path/macrocore/base");
options insert=(sasautos="/your/path/macrocore/meta");
```
The above can be done directly in your sas program, via an autoexec, or an initialisation program.
Alternatively - for quick access - simply run the following! This file contains all the macros.
```sas
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
```
# Standards
## File Properties
- filenames much match macro names
- filenames must be lowercase
- macro names must be lowercase
- one macro per file
- prefixes:
- _mf_ for macro functions (can be used in open code).
- _mp_ for macro procedures (which generate sas code)
- _mm_ for metadata macros (interface with the metadata server).
- _mmx_ for macros that use metadata and are XCMD enabled
- _mx_ for macros that are XCMD enabled
- _mv_ for macros that will only work in Viya
- follow verb-noun convention
- unix style line endings (lf)
- individual lines should be no more than 80 characters long
- UTF-8
- no trailing white space
## Header Properties
The **Macro Core** documentation is created using [doxygen](http://www.doxygen.nl). A full list of attributes can be found [here](http://www.doxygen.nl/manual/commands.html) but the following are most relevant:
- file. This needs to be present in order to be recognised by doxygen.
- brief. This is a short (one sentence) description of the macro.
- details. A longer description, which can contain doxygen [markdown](http://www.stack.nl/~dimitri/doxygen/manual/markdown.html).
- param. Name of each input param followed by a description.
- return. Explanation of what is returned by the macro.
- version. The EARLIEST SAS version in which this macro is known to work.
- author. Author name, contact details optional
All macros must be commented in the doxygen format, to enable the [online documentation](https://sasjs.github.io/core.github.io/).
## Coding Standards
- Indentation = 2 spaces. No tabs!
- Macro variables should not have the trailing dot (`&var` not `&var.`) unless necessary to prevent incorrect resolution
- The closing `%mend;` should not contain the macro name.
- All macros should be defined with brackets, even if no variables are needed - ie `%macro x();` not `%macro x;`
- Mandatory parameters should be positional, all optional parameters should be keyword (var=) style.
- All dataset references must be 2 level (eg `work.blah`, not `blah`). This is to avoid contention when options [DATASTMTCHK](https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000279064.htm)=ALLKEYWORDS is in effect.
- Avoid naming collisions! All macro variables should be local scope. Use system generated work tables where possible - eg `data ; set sashelp.class; run; data &output; set &syslast; run;`
# General Notes
- All macros should be compatible with SAS versions from support level B and above (so currently 9.2 and later). If an earlier version is not supported, then the macro should say as such in the header documentation, and exit gracefully (eg `%if %sysevalf(&sysver<9.3) %then %return`).

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;

94
build.py Executable file
View File

@@ -0,0 +1,94 @@
import os
from pathlib import Path
# Prepare Lua Macros
files = [f for f in Path('lua').iterdir() if f.match("*.lua")]
for file in files:
basename=os.path.basename(file)
name='ml_' + os.path.splitext(basename)[0]
ml = open('lua/' + name + '.sas', "w")
ml.write("/**\n")
ml.write(" @file " + name + '.sas\n')
ml.write(" @brief Creates the " + basename + " file\n")
ml.write(" @details Writes " + basename + " to the work directory\n")
ml.write(" Usage:\n\n")
ml.write(" %" + name + "()\n\n")
ml.write("**/\n\n")
ml.write("%macro " + name + "();\n")
ml.write("data _null_;\n")
ml.write(" file \"%sysfunc(pathname(work))/" + basename + "\";\n")
with open(file) as infile:
for line in infile:
ml.write(" put '" + line.rstrip().replace("'","''") + " ';\n")
ml.write("run;\n")
ml.write("%mend;\n")
ml.close()
# prepare web files
files=['viya/mv_createwebservice.sas','meta/mm_createwebservice.sas']
for file in files:
webout0=open('base/mp_jsonout.sas','r')
if file=='viya/mv_createwebservice.sas':
webout1=open('viya/mv_webout.sas',"r")
else:
webout1=open('meta/mm_webout.sas','r')
webout2=open('base/mf_getuser.sas','r')
outfile=open(file + 'TEMP','w')
infile=open(file,'r')
delrow=0
for line in infile:
if line=='/* WEBOUT BEGIN */\n':
delrow=1
outfile.write('/* WEBOUT BEGIN */\n')
weboutfiles=[webout0,webout1,webout2]
for weboutfile in weboutfiles:
stripcomment=1
for w in weboutfile:
if w=='**/\n': stripcomment=0
elif stripcomment==0:
outfile.write(" put '" + w.rstrip().replace("'","''") + " ';\n")
elif delrow==1 and line=='/* WEBOUT END */\n':
delrow=0
outfile.write('/* WEBOUT END */\n')
elif delrow==0:
outfile.write(line.rstrip() + "\n")
webout0.close()
webout1.close()
outfile.close()
infile.close()
os.remove(file)
os.rename(file + 'TEMP',file)
# Concatenate all macros into a single file
header="""
/**
@file
@brief Auto-generated file
@details
This file contains all the macros in a single file - which means it can be
'included' in SAS with just 2 lines of code:
filename mc url
"https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
The `build.py` file in the https://github.com/macropeople/macrocore repo
is used to create this file.
@author Allan Bowe
**/
options noquotelenmax;
"""
f = open('mc_all.sas', "w") # r / r+ / rb / rb+ / w / wb
f.write(header)
folders=['base','meta','metax','viya','lua']
for folder in folders:
filenames = [fn for fn in Path('./' + folder).iterdir() if fn.match("*.sas")]
filenames.sort()
with open('mc_' + folder + '.sas', 'w') as outfile:
for fname in filenames:
with open(fname) as infile:
outfile.write(infile.read())
with open('mc_' + folder + '.sas','r') as c:
f.write(c.read())
f.close()

37
doxy/Doxyfile Normal file
View File

@@ -0,0 +1,37 @@
ALPHABETICAL_INDEX = NO
DISABLE_INDEX = YES
ENABLE_PREPROCESSING = NO
EXTENSION_MAPPING = sas=Java ddl=Java
EXTRACT_LOCAL_CLASSES = NO
FILE_PATTERNS = *.sas \
*.ddl \
*.dox
GENERATE_LATEX = NO
GENERATE_TREEVIEW = YES
HIDE_FRIEND_COMPOUNDS = YES
HIDE_IN_BODY_DOCS = YES
HIDE_SCOPE_NAMES = YES
HIDE_UNDOC_CLASSES = YES
HIDE_UNDOC_MEMBERS = YES
HTML_OUTPUT = doxy
HTML_HEADER = ./doxy/new_header.html
HTML_FOOTER = ./doxy/new_footer.html
HTML_EXTRA_STYLESHEET = ./doxy/new_stylesheet.css
INHERIT_DOCS = NO
INLINE_INFO = NO
INPUT = base meta metax viya
LAYOUT_FILE = ./doxy/DoxygenLayout.xml
MAX_INITIALIZER_LINES = 0
PROJECT_NAME = Macro Core
PROJECT_LOGO = doxy/Macro_core_website_1.png
RECURSIVE = YES
REPEAT_BRIEF = NO
SHOW_NAMESPACES = NO
SHOW_USED_FILES = NO
SOURCE_BROWSER = YES
SOURCE_TOOLTIPS = NO
STRICT_PROTO_MATCHING = YES
STRIP_CODE_COMMENTS = NO
SUBGROUPING = NO
TAB_SIZE = 2
VERBATIM_HEADERS = NO

111
doxy/DoxygenLayout.xml Normal file
View File

@@ -0,0 +1,111 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.14 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="no" title=""/>
<tab type="pages" visible="no" title="" intro=""/>
<tab type="modules" visible="no" title="" intro=""/>
<tab type="namespaces" visible="no" title="">
<tab type="namespacelist" visible="no" title="" intro=""/>
<tab type="namespacemembers" visible="no" title="" intro=""/>
</tab>
<tab type="classes" visible="no" title="">
<tab type="classlist" visible="no" title="" intro=""/>
<tab type="classindex" visible="no" title=""/>
<tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/>
</tab>
<tab type="filelist" visible="yes" title="" intro="List of Files Used in the Macro Core Library"/>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
<!-- Layout definition for a file page -->
<file>
<briefdescription visible="yes"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<includegraph visible="$INCLUDE_GRAPH"/>
<includedbygraph visible="$INCLUDED_BY_GRAPH"/>
<sourcelink visible="yes"/>
<memberdecl>
<classes visible="no" title=""/>
<namespaces visible="no" title=""/>
<constantgroups visible="no" title=""/>
<defines title=""/>
<typedefs title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
<membergroups visible="no"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<inlineclasses title=""/>
<defines title=""/>
<typedefs title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
</memberdef>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="no"/>
<groupgraph visible="$GROUP_GRAPHS"/>
<memberdecl>
<nestedgroups visible="no" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="no" title=""/>
<classes visible="no" title=""/>
<defines title=""/>
<typedefs title=""/>
<enums title=""/>
<enumvalues title=""/>
<functions title=""/>
<variables title=""/>
<signals title=""/>
<publicslots title=""/>
<protectedslots title=""/>
<privateslots title=""/>
<events title=""/>
<properties title=""/>
<friends title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<pagedocs/>
<inlineclasses title=""/>
<defines title=""/>
<typedefs title=""/>
<enums title=""/>
<enumvalues title=""/>
<functions title=""/>
<variables title=""/>
<signals title=""/>
<publicslots title=""/>
<protectedslots title=""/>
<privateslots title=""/>
<events title=""/>
<properties title=""/>
<friends title=""/>
</memberdef>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="yes"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
</directory>
</doxygenlayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

6
doxy/batch.bat Normal file
View File

@@ -0,0 +1,6 @@
ECHO on
rmdir /s /q C:\DEVELOPMENT\output
mkdir C:\DEVELOPMENT\output
doxygen.exe Doxyfile

40
doxy/build.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
####################################################################
# PROJECT: Macro Core Docs Build #
####################################################################
BUILD_FOLDER="/tmp/macrocore_docs"
# move to project root
cd ..
# create build directory
rm -rf $BUILD_FOLDER
mkdir $BUILD_FOLDER
# copy relevant files
cp -r base $BUILD_FOLDER
cp -r meta $BUILD_FOLDER
cp -r metax $BUILD_FOLDER
cp -r viya $BUILD_FOLDER
cp -r doxy $BUILD_FOLDER
cp main.dox $BUILD_FOLDER
cp doxy/Doxyfile $BUILD_FOLDER
# update Doxyfile and generate
cd $BUILD_FOLDER
echo "OUTPUT_DIRECTORY=$BUILD_FOLDER/out" >> $BUILD_FOLDER/Doxyfile
echo "INPUT+=main.dox" >> $BUILD_FOLDER/Doxyfile
doxygen Doxyfile
# refresh github pages site
git clone git@github.com:macropeople/macrocore.github.io.git
cd macrocore.github.io
git rm -r *
mv $BUILD_FOLDER/out/doxy/* .
echo 'core.sasjs.io' > CNAME
git add *
git commit -m "build.sh build on $(date +%F:%H:%M:%S)"
git push
echo "check it out: https://macropeople.github.io/macrocore.github.io/files.html"

BIN
doxy/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

22
doxy/new_footer.html Normal file
View File

@@ -0,0 +1,22 @@
<!--BEGIN GENERATE_TREEVIEW-->
<li class="footer"><b>$generatedby</b>
<a href="http://www.doxygen.org/index.html">
<img class="footer" src="doxygen.png" alt="doxygen"/></a>
<i> For more information visit the </i> <a href="https://github.com/macropeople/macrocore">macropeople MacroCore library</a>.</li>
</ul>
</div>
<!--END GENERATE_TREEVIEW-->
<!--BEGIN !GENERATE_TREEVIEW-->
<hr class="footer"/>
<table width="100%"><tbody><tr><td>
For more information visit the <a href="https://github.com/macropeople/macrocore">macropeople MacroCore library</a>.
</td><td><address class="footer"><small>
&copy;$year<br/>
$generatedby <a href="http://www.doxygen.org/index.html">
<!--<img class="footer" src="$relpath$doxygen.png" alt="doxygen"/>-->doxygen
</a> $doxygenversion
</small></address></tr></tbody></table>
<!--END !GENERATE_TREEVIEW-->
</body>
</html>

72
doxy/new_header.html Normal file
View File

@@ -0,0 +1,72 @@
<!-- HTML header for doxygen 1.8.14-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="$relpath^jquery.js"></script>
<script type="text/javascript" src="$relpath^dynsections.js"></script>
$treeview
$search
$mathjax
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
<link REL="icon" HREF="https://sasjs.io/img/runningman.jpg">
$extrastylesheet
</head>
<body>
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<!--BEGIN TITLEAREA-->
<div id="titlearea" style='background-color:white' >
<table cellspacing="0" cellpadding="0">
<tbody>
<tr style="height: 26px;">
<!--BEGIN PROJECT_LOGO-->
<td id="projectlogo"></td>
<!--END PROJECT_LOGO-->
<!--BEGIN PROJECT_NAME-->
<td>
<div id="projectname">
<!--a href=".">
<img alt="Macro Core" src="https://macropeople.com/wp-content/uploads/2018/05/macropeople2014retina_V2.png" height=60/>
</a-->
<a href=".">
<img alt="Macro Core" src="./Macro_core_website_1.png" height=60/>
</a>
</div>
</td>
<td>
<table style="padding-left: 2em;" cellspacing="0" cellpadding="0">
<tr><td> Production Ready Macros for SAS Application Developers</td></tr>
<tr><td><a href="https://github.com/macropeople/macrocore">
https://github.com/macropeople/macrocore
</a></td></tr>
</table>
</td>
<!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME-->
<!--BEGIN PROJECT_BRIEF-->
<!--END PROJECT_BRIEF-->
<!--END !PROJECT_NAME-->
<div class="header">
<div class="headertitle">
<!--BEGIN DISABLE_INDEX-->
<!--BEGIN SEARCHENGINE-->
<td>$searchbox</td>
<!--END SEARCHENGINE-->
<!--END DISABLE_INDEX-->
</tr>
</tbody>
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part -->

1595
doxy/new_stylesheet.css Normal file

File diff suppressed because it is too large Load Diff

BIN
doxy/runningman.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

455
lua/json2sas.lua Normal file
View File

@@ -0,0 +1,455 @@
--
-- json2sas.lua (modified from json.lua)
--
-- Copyright (c) 2019 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json2sas = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json2sas.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json2sas.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
-- convert macro variable array into one variable and decode
function json2sas.go(macvar)
local x=1
local cnt=0
local mac=sas.symget(macvar..'0')
local newstr=''
if mac and mac ~= '' then
cnt=mac
for x=1,cnt,1 do
mac=sas.symget(macvar..x)
if mac and mac ~= '' then
newstr=newstr..mac
else
return print(macvar..x..' NOT FOUND!!')
end
end
else
return print(macvar..'0 NOT FOUND!!')
end
-- print('mac:'..mac..'cnt:'..cnt..'newstr'..newstr)
local oneVar=json2sas.decode(newstr)
local jsdata=oneVar["data"]
local meta={}
local attrs={}
for tablename, data in pairs(jsdata) do -- each table
print("Processing table: "..tablename)
attrs[tablename]={}
for k, v in ipairs(data) do -- each row
if(k==1) then -- column names
for a, b in pairs(v) do
attrs[tablename][a]={}
attrs[tablename][a]["name"]=b
end
elseif(k==2) then -- get types
for a, b in pairs(v) do
if type(b)=='number' then
attrs[tablename][a]["type"]="N"
attrs[tablename][a]["length"]=8
else
attrs[tablename][a]["type"]="C"
attrs[tablename][a]["length"]=string.len(b)
end
end
else --update lengths
for a, b in pairs(v) do
if (type(b)=='string' and string.len(b)>attrs[tablename][a]["length"])
then
attrs[tablename][a]["length"]=string.len(b)
end
end
end
end
print(json2sas.encode(attrs[tablename])) -- show results
-- Now create the SAS table
sas.new_table("work."..tablename,attrs[tablename])
local dsid=sas.open("work."..tablename, "u")
for k, v in ipairs(data) do
if k>1 then
sas.append(dsid)
for a, b in pairs(v) do
sas.put_value(dsid, attrs[tablename][a]["name"], b)
end
sas.update(dsid)
end
end
sas.close(dsid)
end
return json2sas.decode(newstr)
end
function quote(str)
return sas.quote(str)
end
function sasvar(str)
print("processing: "..str)
print(sas.symexist(str))
if sas.symexist(str)==1 then
return quote(str)..':'..quote(sas.symget(str))..','
end
return ''
end
return json2sas

470
lua/ml_json2sas.sas Normal file
View File

@@ -0,0 +1,470 @@
/**
@file ml_json2sas.sas
@brief Creates the json2sas.lua file
@details Writes json2sas.lua to the work directory
Usage:
%ml_json2sas()
**/
%macro ml_json2sas();
data _null_;
file "%sysfunc(pathname(work))/json2sas.lua";
put '-- ';
put '-- json2sas.lua (modified from json.lua) ';
put '-- ';
put '-- Copyright (c) 2019 rxi ';
put '-- ';
put '-- Permission is hereby granted, free of charge, to any person obtaining a copy of ';
put '-- this software and associated documentation files (the "Software"), to deal in ';
put '-- the Software without restriction, including without limitation the rights to ';
put '-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies ';
put '-- of the Software, and to permit persons to whom the Software is furnished to do ';
put '-- so, subject to the following conditions: ';
put '-- ';
put '-- The above copyright notice and this permission notice shall be included in all ';
put '-- copies or substantial portions of the Software. ';
put '-- ';
put '-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ';
put '-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ';
put '-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ';
put '-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ';
put '-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ';
put '-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ';
put '-- SOFTWARE. ';
put '-- ';
put ' ';
put 'local json2sas = { _version = "0.1.2" } ';
put ' ';
put '------------------------------------------------------------------------------- ';
put '-- Encode ';
put '------------------------------------------------------------------------------- ';
put ' ';
put 'local encode ';
put ' ';
put 'local escape_char_map = { ';
put ' [ "\\" ] = "\\\\", ';
put ' [ "\"" ] = "\\\"", ';
put ' [ "\b" ] = "\\b", ';
put ' [ "\f" ] = "\\f", ';
put ' [ "\n" ] = "\\n", ';
put ' [ "\r" ] = "\\r", ';
put ' [ "\t" ] = "\\t", ';
put '} ';
put ' ';
put 'local escape_char_map_inv = { [ "\\/" ] = "/" } ';
put 'for k, v in pairs(escape_char_map) do ';
put ' escape_char_map_inv[v] = k ';
put 'end ';
put ' ';
put 'local function escape_char(c) ';
put ' return escape_char_map[c] or string.format("\\u%04x", c:byte()) ';
put 'end ';
put ' ';
put 'local function encode_nil(val) ';
put ' return "null" ';
put 'end ';
put ' ';
put 'local function encode_table(val, stack) ';
put ' local res = {} ';
put ' stack = stack or {} ';
put ' ';
put ' -- Circular reference? ';
put ' if stack[val] then error("circular reference") end ';
put ' ';
put ' stack[val] = true ';
put ' ';
put ' if rawget(val, 1) ~= nil or next(val) == nil then ';
put ' -- Treat as array -- check keys are valid and it is not sparse ';
put ' local n = 0 ';
put ' for k in pairs(val) do ';
put ' if type(k) ~= "number" then ';
put ' error("invalid table: mixed or invalid key types") ';
put ' end ';
put ' n = n + 1 ';
put ' end ';
put ' if n ~= #val then ';
put ' error("invalid table: sparse array") ';
put ' end ';
put ' -- Encode ';
put ' for i, v in ipairs(val) do ';
put ' table.insert(res, encode(v, stack)) ';
put ' end ';
put ' stack[val] = nil ';
put ' return "[" .. table.concat(res, ",") .. "]" ';
put ' else ';
put ' -- Treat as an object ';
put ' for k, v in pairs(val) do ';
put ' if type(k) ~= "string" then ';
put ' error("invalid table: mixed or invalid key types") ';
put ' end ';
put ' table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) ';
put ' end ';
put ' stack[val] = nil ';
put ' return "{" .. table.concat(res, ",") .. "}" ';
put ' end ';
put 'end ';
put ' ';
put 'local function encode_string(val) ';
put ' return ''"'' .. val:gsub(''[%z\1-\31\\"]'', escape_char) .. ''"'' ';
put 'end ';
put ' ';
put 'local function encode_number(val) ';
put ' -- Check for NaN, -inf and inf ';
put ' if val ~= val or val <= -math.huge or val >= math.huge then ';
put ' error("unexpected number value ''" .. tostring(val) .. "''") ';
put ' end ';
put ' return string.format("%.14g", val) ';
put 'end ';
put ' ';
put 'local type_func_map = { ';
put ' [ "nil" ] = encode_nil, ';
put ' [ "table" ] = encode_table, ';
put ' [ "string" ] = encode_string, ';
put ' [ "number" ] = encode_number, ';
put ' [ "boolean" ] = tostring, ';
put '} ';
put ' ';
put 'encode = function(val, stack) ';
put ' local t = type(val) ';
put ' local f = type_func_map[t] ';
put ' if f then ';
put ' return f(val, stack) ';
put ' end ';
put ' error("unexpected type ''" .. t .. "''") ';
put 'end ';
put ' ';
put 'function json2sas.encode(val) ';
put ' return ( encode(val) ) ';
put 'end ';
put ' ';
put '------------------------------------------------------------------------------- ';
put '-- Decode ';
put '------------------------------------------------------------------------------- ';
put 'local parse ';
put 'local function create_set(...) ';
put ' local res = {} ';
put ' for i = 1, select("#", ...) do ';
put ' res[ select(i, ...) ] = true ';
put ' end ';
put ' return res ';
put 'end ';
put ' ';
put 'local space_chars = create_set(" ", "\t", "\r", "\n") ';
put 'local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") ';
put 'local escape_chars = create_set("\\", "/", ''"'', "b", "f", "n", "r", "t", "u") ';
put 'local literals = create_set("true", "false", "null") ';
put ' ';
put 'local literal_map = { ';
put ' [ "true" ] = true, ';
put ' [ "false" ] = false, ';
put ' [ "null" ] = nil, ';
put '} ';
put ' ';
put 'local function next_char(str, idx, set, negate) ';
put ' for i = idx, #str do ';
put ' if set[str:sub(i, i)] ~= negate then ';
put ' return i ';
put ' end ';
put ' end ';
put ' return #str + 1 ';
put 'end ';
put ' ';
put 'local function decode_error(str, idx, msg) ';
put ' local line_count = 1 ';
put ' local col_count = 1 ';
put ' for i = 1, idx - 1 do ';
put ' col_count = col_count + 1 ';
put ' if str:sub(i, i) == "\n" then ';
put ' line_count = line_count + 1 ';
put ' col_count = 1 ';
put ' end ';
put ' end ';
put ' error( string.format("%s at line %d col %d", msg, line_count, col_count) ) ';
put 'end ';
put ' ';
put 'local function codepoint_to_utf8(n) ';
put ' -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa ';
put ' local f = math.floor ';
put ' if n <= 0x7f then ';
put ' return string.char(n) ';
put ' elseif n <= 0x7ff then ';
put ' return string.char(f(n / 64) + 192, n % 64 + 128) ';
put ' elseif n <= 0xffff then ';
put ' return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) ';
put ' elseif n <= 0x10ffff then ';
put ' return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, ';
put ' f(n % 4096 / 64) + 128, n % 64 + 128) ';
put ' end ';
put ' error( string.format("invalid unicode codepoint ''%x''", n) ) ';
put 'end ';
put ' ';
put 'local function parse_unicode_escape(s) ';
put ' local n1 = tonumber( s:sub(3, 6), 16 ) ';
put ' local n2 = tonumber( s:sub(9, 12), 16 ) ';
put ' -- Surrogate pair? ';
put ' if n2 then ';
put ' return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) ';
put ' else ';
put ' return codepoint_to_utf8(n1) ';
put ' end ';
put 'end ';
put ' ';
put 'local function parse_string(str, i) ';
put ' local has_unicode_escape = false ';
put ' local has_surrogate_escape = false ';
put ' local has_escape = false ';
put ' local last ';
put ' for j = i + 1, #str do ';
put ' local x = str:byte(j) ';
put ' if x < 32 then ';
put ' decode_error(str, j, "control character in string") ';
put ' end ';
put ' if last == 92 then -- "\\" (escape char) ';
put ' if x == 117 then -- "u" (unicode escape sequence) ';
put ' local hex = str:sub(j + 1, j + 5) ';
put ' if not hex:find("%x%x%x%x") then ';
put ' decode_error(str, j, "invalid unicode escape in string") ';
put ' end ';
put ' if hex:find("^[dD][89aAbB]") then ';
put ' has_surrogate_escape = true ';
put ' else ';
put ' has_unicode_escape = true ';
put ' end ';
put ' else ';
put ' local c = string.char(x) ';
put ' if not escape_chars[c] then ';
put ' decode_error(str, j, "invalid escape char ''" .. c .. "'' in string") ';
put ' end ';
put ' has_escape = true ';
put ' end ';
put ' last = nil ';
put ' elseif x == 34 then -- ''"'' (end of string) ';
put ' local s = str:sub(i + 1, j - 1) ';
put ' if has_surrogate_escape then ';
put ' s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) ';
put ' end ';
put ' if has_unicode_escape then ';
put ' s = s:gsub("\\u....", parse_unicode_escape) ';
put ' end ';
put ' if has_escape then ';
put ' s = s:gsub("\\.", escape_char_map_inv) ';
put ' end ';
put ' return s, j + 1 ';
put ' else ';
put ' last = x ';
put ' end ';
put ' end ';
put ' decode_error(str, i, "expected closing quote for string") ';
put 'end ';
put ' ';
put 'local function parse_number(str, i) ';
put ' local x = next_char(str, i, delim_chars) ';
put ' local s = str:sub(i, x - 1) ';
put ' local n = tonumber(s) ';
put ' if not n then ';
put ' decode_error(str, i, "invalid number ''" .. s .. "''") ';
put ' end ';
put ' return n, x ';
put 'end ';
put ' ';
put 'local function parse_literal(str, i) ';
put ' local x = next_char(str, i, delim_chars) ';
put ' local word = str:sub(i, x - 1) ';
put ' if not literals[word] then ';
put ' decode_error(str, i, "invalid literal ''" .. word .. "''") ';
put ' end ';
put ' return literal_map[word], x ';
put 'end ';
put ' ';
put 'local function parse_array(str, i) ';
put ' local res = {} ';
put ' local n = 1 ';
put ' i = i + 1 ';
put ' while 1 do ';
put ' local x ';
put ' i = next_char(str, i, space_chars, true) ';
put ' -- Empty / end of array? ';
put ' if str:sub(i, i) == "]" then ';
put ' i = i + 1 ';
put ' break ';
put ' end ';
put ' -- Read token ';
put ' x, i = parse(str, i) ';
put ' res[n] = x ';
put ' n = n + 1 ';
put ' -- Next token ';
put ' i = next_char(str, i, space_chars, true) ';
put ' local chr = str:sub(i, i) ';
put ' i = i + 1 ';
put ' if chr == "]" then break end ';
put ' if chr ~= "," then decode_error(str, i, "expected '']'' or '',''") end ';
put ' end ';
put ' return res, i ';
put 'end ';
put ' ';
put 'local function parse_object(str, i) ';
put ' local res = {} ';
put ' i = i + 1 ';
put ' while 1 do ';
put ' local key, val ';
put ' i = next_char(str, i, space_chars, true) ';
put ' -- Empty / end of object? ';
put ' if str:sub(i, i) == "}" then ';
put ' i = i + 1 ';
put ' break ';
put ' end ';
put ' -- Read key ';
put ' if str:sub(i, i) ~= ''"'' then ';
put ' decode_error(str, i, "expected string for key") ';
put ' end ';
put ' key, i = parse(str, i) ';
put ' -- Read '':'' delimiter ';
put ' i = next_char(str, i, space_chars, true) ';
put ' if str:sub(i, i) ~= ":" then ';
put ' decode_error(str, i, "expected '':'' after key") ';
put ' end ';
put ' i = next_char(str, i + 1, space_chars, true) ';
put ' -- Read value ';
put ' val, i = parse(str, i) ';
put ' -- Set ';
put ' res[key] = val ';
put ' -- Next token ';
put ' i = next_char(str, i, space_chars, true) ';
put ' local chr = str:sub(i, i) ';
put ' i = i + 1 ';
put ' if chr == "}" then break end ';
put ' if chr ~= "," then decode_error(str, i, "expected ''}'' or '',''") end ';
put ' end ';
put ' return res, i ';
put 'end ';
put ' ';
put 'local char_func_map = { ';
put ' [ ''"'' ] = parse_string, ';
put ' [ "0" ] = parse_number, ';
put ' [ "1" ] = parse_number, ';
put ' [ "2" ] = parse_number, ';
put ' [ "3" ] = parse_number, ';
put ' [ "4" ] = parse_number, ';
put ' [ "5" ] = parse_number, ';
put ' [ "6" ] = parse_number, ';
put ' [ "7" ] = parse_number, ';
put ' [ "8" ] = parse_number, ';
put ' [ "9" ] = parse_number, ';
put ' [ "-" ] = parse_number, ';
put ' [ "t" ] = parse_literal, ';
put ' [ "f" ] = parse_literal, ';
put ' [ "n" ] = parse_literal, ';
put ' [ "[" ] = parse_array, ';
put ' [ "{" ] = parse_object, ';
put '} ';
put ' ';
put 'parse = function(str, idx) ';
put ' local chr = str:sub(idx, idx) ';
put ' local f = char_func_map[chr] ';
put ' if f then ';
put ' return f(str, idx) ';
put ' end ';
put ' decode_error(str, idx, "unexpected character ''" .. chr .. "''") ';
put 'end ';
put ' ';
put 'function json2sas.decode(str) ';
put ' if type(str) ~= "string" then ';
put ' error("expected argument of type string, got " .. type(str)) ';
put ' end ';
put ' local res, idx = parse(str, next_char(str, 1, space_chars, true)) ';
put ' idx = next_char(str, idx, space_chars, true) ';
put ' if idx <= #str then ';
put ' decode_error(str, idx, "trailing garbage") ';
put ' end ';
put ' return res ';
put 'end ';
put ' ';
put '-- convert macro variable array into one variable and decode ';
put 'function json2sas.go(macvar) ';
put ' local x=1 ';
put ' local cnt=0 ';
put ' local mac=sas.symget(macvar..''0'') ';
put ' local newstr='''' ';
put ' if mac and mac ~= '''' then ';
put ' cnt=mac ';
put ' for x=1,cnt,1 do ';
put ' mac=sas.symget(macvar..x) ';
put ' if mac and mac ~= '''' then ';
put ' newstr=newstr..mac ';
put ' else ';
put ' return print(macvar..x..'' NOT FOUND!!'') ';
put ' end ';
put ' end ';
put ' else ';
put ' return print(macvar..''0 NOT FOUND!!'') ';
put ' end ';
put ' -- print(''mac:''..mac..''cnt:''..cnt..''newstr''..newstr) ';
put ' local oneVar=json2sas.decode(newstr) ';
put ' local jsdata=oneVar["data"] ';
put ' local meta={} ';
put ' local attrs={} ';
put ' for tablename, data in pairs(jsdata) do -- each table ';
put ' print("Processing table: "..tablename) ';
put ' attrs[tablename]={} ';
put ' for k, v in ipairs(data) do -- each row ';
put ' if(k==1) then -- column names ';
put ' for a, b in pairs(v) do ';
put ' attrs[tablename][a]={} ';
put ' attrs[tablename][a]["name"]=b ';
put ' end ';
put ' elseif(k==2) then -- get types ';
put ' for a, b in pairs(v) do ';
put ' if type(b)==''number'' then ';
put ' attrs[tablename][a]["type"]="N" ';
put ' attrs[tablename][a]["length"]=8 ';
put ' else ';
put ' attrs[tablename][a]["type"]="C" ';
put ' attrs[tablename][a]["length"]=string.len(b) ';
put ' end ';
put ' end ';
put ' else --update lengths ';
put ' for a, b in pairs(v) do ';
put ' if (type(b)==''string'' and string.len(b)>attrs[tablename][a]["length"]) ';
put ' then ';
put ' attrs[tablename][a]["length"]=string.len(b) ';
put ' end ';
put ' end ';
put ' end ';
put ' end ';
put ' print(json2sas.encode(attrs[tablename])) -- show results ';
put ' ';
put ' -- Now create the SAS table ';
put ' sas.new_table("work."..tablename,attrs[tablename]) ';
put ' local dsid=sas.open("work."..tablename, "u") ';
put ' for k, v in ipairs(data) do ';
put ' if k>1 then ';
put ' sas.append(dsid) ';
put ' for a, b in pairs(v) do ';
put ' sas.put_value(dsid, attrs[tablename][a]["name"], b) ';
put ' end ';
put ' sas.update(dsid) ';
put ' end ';
put ' end ';
put ' sas.close(dsid) ';
put ' end ';
put ' return json2sas.decode(newstr) ';
put 'end ';
put ' ';
put ' ';
put 'function quote(str) ';
put ' return sas.quote(str) ';
put 'end ';
put 'function sasvar(str) ';
put ' print("processing: "..str) ';
put ' print(sas.symexist(str)) ';
put ' if sas.symexist(str)==1 then ';
put ' return quote(str)..'':''..quote(sas.symget(str))..'','' ';
put ' end ';
put ' return '''' ';
put 'end ';
put ' ';
put 'return json2sas ';
run;
%mend;

48
main.dox Normal file
View File

@@ -0,0 +1,48 @@
/** \dir .
* \brief Open Source Macro Library for Developers of the SAS Language. See: https://github.com/macropeople/macrocore
* \details To use - add the subfolders to your `SASAUTOS` call path.
*/
/*! \dir base
* \brief Regular Base SAS Macros
* \details These macros have the following attributes:
* OS independent
* Not metadata aware
* No X command
* Prefixes: _mf_, _mp_
*/
/*! \dir meta
* \brief Metadata Aware Macros
* \details These macros have the following attributes:
* OS independent
* Metadata aware
* No X command
* Prefixes: _mm_
*/
/*! \dir metax
* \brief Metadata Aware Macros with X commmand
* \details These macros have the following attributes:
* OS independent
* Metadata aware
* X command
* Prefixes: _mmx_, _mmw_, _mmu_
*/
/*! \dir viya
* \brief Viya macros
* \details These macros have the following attributes:
* OS independent
* No X command
* Prefixes: _mv_
*/

29
make_singlefile.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Concatenate all macros into a single file
OUTFILE='./macrocore.sas'
cat > $OUTFILE <<'EOL'
/**
@file
@brief Auto-generated file
@details
This file contains all the macros in a single file - which means it can be
'included' in SAS with just 2 lines of code:
filename mc url
"https://raw.githubusercontent.com/macropeople/macrocore/master/macrocore.sas";
%inc mc;
The `build.sh` file in the https://github.com/macropeople/macrocore repo
is used to create this file.
@author Allan Bowe
**/
EOL
cat base/* >> $OUTFILE
cat meta/* >> $OUTFILE
cat metax/* >> $OUTFILE
cat viya/* >> $OUTFILE

12604
mc_all.sas Normal file

File diff suppressed because it is too large Load Diff

97
meta/mm_adduser2group.sas Normal file
View File

@@ -0,0 +1,97 @@
/**
@file
@brief Adds a user to a group
@details Adds a user to a metadata group. The macro first checks whether the
user is in that group, and if not, the user is added.
Usage:
%mm_adduser2group(user=sasdemo
,group=someGroup)
@param user= the user name (not displayname)
@param group= the group to which to add the user
@warning the macro does not check inherited group memberships - it looks at
direct members only
@version 9.3
@author Allan Bowe
**/
%macro mm_adduser2group(user=
,group=
,mdebug=0
);
/* first, check if user is in group already exists */
%local check uuri guri;
%let check=ok;
data _null_;
length uri type msg $256;
call missing(of _all_);
rc=metadata_getnobj("omsobj:Person?@Name='&user'",1,uri);
if rc<=0 then do;
msg="%str(WARN)ING: rc="!!cats(rc)!!" &user not found "!!
", or there was an err reading the repository.";
call symputx('check',msg);
putlog msg;
stop;
end;
call symputx('uuri',scan(uri,2,'\'));
rc=metadata_getnobj("omsobj:IdentityGroup?@Name='&group'",1,uri);
if rc<=0 then do;
msg="%str(WARN)ING: rc="!!cats(rc)!!" &group not found "!!
", or there was an err reading the repository.";
call symputx('check',msg);
putlog msg;
stop;
end;
call symputx('guri',scan(uri,2,'\'));
rc=metadata_getnobj("omsobj:Person?Person[@Name='&user'][IdentityGroups/*[@Name='&group']]",1,uri);
if rc=0 then do;
msg="%str(WARN)ING: rc="!!cats(rc)!!" &user already in &group";
call symputx('check',msg);
stop;
end;
if &mdebug ne 0 then put (_all_)(=);
run;
/* stop if issues */
%if %quote(&check) ne %quote(ok) %then %do;
%put &check;
%return;
%end;
%if %length(&syscc) ge 4 %then %do;
%put WARNING: SYSCC=&syscc, exiting &sysmacroname;
%return;
%end;
filename __us2grp temp;
proc metadata in= "<UpdateMetadata><Reposid>$METAREPOSITORY</Reposid><Metadata>
<Person Id='&uuri'><IdentityGroups><IdentityGroup ObjRef='&guri' />
</IdentityGroups></Person></Metadata>
<NS>SAS</NS><Flags>268435456</Flags></UpdateMetadata>"
out=__us2grp verbose;
run;
%if &mdebug ne 0 %then %do;
/* write the response to the log for debugging */
data _null_;
infile __us2grp lrecl=32767;
input;
put _infile_;
run;
%end;
filename __us2grp clear;
%mend;

452
meta/mm_assigndirectlib.sas Executable file
View File

@@ -0,0 +1,452 @@
/**
@file
@brief Assigns library directly using details from metadata
@details Queries metadata to get the libname definition then allocates the
library directly (ie, not using the META engine).
usage:
%mm_assignDirectLib(MyLib);
data x; set mylib.sometable; run;
%mm_assignDirectLib(MyDB,open_passthrough=MyAlias);
create table MyTable as
select * from connection to MyAlias( select * from DBTable);
disconnect from MyAlias;
quit;
<h4> Dependencies </h4>
@li mf_getengine.sas
@li mp_abort.sas
@param libref the libref (not name) of the metadata library
@param open_passthrough= provide an alias to produce the CONNECT TO statement
for the relevant external database
@param sql_options= an override default output fileref to avoid naming clash
@param mDebug= set to 1 to show debug messages in the log
@param mAbort= set to 1 to call %mp_abort().
@returns libname statement
@version 9.2
@author Allan Bowe
**/
%macro mm_assigndirectlib(
libref /* libref to assign from metadata */
,open_passthrough= /* provide an alias to produce the
CONNECT TO statement for the
relevant external database */
,sql_options= /* add any options to add to proc sql statement eg outobs=
(only valid for pass through) */
,mDebug=0
,mAbort=0
)/*/STORE SOURCE*/;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing mm_assigndirectlib.sas;
%&mD.put _local_;
%if &mAbort=1 %then %let mAbort=;
%else %let mAbort=%str(*);
%&mD.put NOTE: Creating direct (non META) connection to &libref library;
%local cur_engine;
%let cur_engine=%mf_getengine(&libref);
%if &cur_engine ne META and &cur_engine ne %then %do;
%put NOTE: &libref already has a direct (&cur_engine) libname connection;
%return;
%end;
%else %if %upcase(&libref)=WORK %then %do;
%put NOTE: We already have a direct connection to WORK :-) ;
%return;
%end;
/* need to determine the library ENGINE first */
%local engine;
data _null_;
length lib_uri engine $256;
call missing (of _all_);
/* get URI for the particular library */
rc1=metadata_getnobj("omsobj:SASLibrary?@Libref ='&libref'",1,lib_uri);
/* get the Engine attribute of the previous object */
rc2=metadata_getattr(lib_uri,'Engine',engine);
putlog "mm_assigndirectlib for &libref:" rc1= lib_uri= rc2= engine=;
call symputx("liburi",lib_uri,'l');
call symputx("engine",engine,'l');
run;
/* now obtain engine specific connection details */
%if &engine=BASE %then %do;
%&mD.put NOTE: Retrieving BASE library path;
data _null_;
length up_uri $256 path cat_path $1024;
retain cat_path;
call missing (of _all_);
/* get all the filepaths of the UsingPackages association */
i=1;
rc3=metadata_getnasn("&liburi",'UsingPackages',i,up_uri);
do while (rc3>0);
/* get the DirectoryName attribute of the previous object */
rc4=metadata_getattr(up_uri,'DirectoryName',path);
if i=1 then path = '("'!!trim(path)!!'" ';
else path =' "'!!trim(path)!!'" ';
cat_path = trim(cat_path) !! " " !! trim(path) ;
i+1;
rc3=metadata_getnasn("&liburi",'UsingPackages',i,up_uri);
end;
cat_path = trim(cat_path) !! ")";
&mD.putlog "NOTE: Getting physical path for &libref library";
&mD.putlog rc3= up_uri= rc4= cat_path= path=;
&mD.putlog "NOTE: Libname cmd will be:";
&mD.putlog "libname &libref" cat_path;
call symputx("filepath",cat_path,'l');
run;
%if %sysevalf(&sysver<9.4) %then %do;
libname &libref &filepath;
%end;
%else %do;
/* apply the new filelocks option to cater for temporary locks */
libname &libref &filepath filelockwait=5;
%end;
%end;
%else %if &engine=REMOTE %then %do;
data x;
length rcCon rcProp rc k 3 uriCon uriProp PropertyValue PropertyName Delimiter $256 properties $2048;
retain properties;
rcCon = metadata_getnasn("&liburi", "LibraryConnection", 1, uriCon);
rcProp = metadata_getnasn(uriCon, "Properties", 1, uriProp);
k = 1;
rcProp = metadata_getnasn(uriCon, "Properties", k, uriProp);
do while (rcProp > 0);
rc = metadata_getattr(uriProp , "DefaultValue",PropertyValue);
rc = metadata_getattr(uriProp , "PropertyName",PropertyName);
rc = metadata_getattr(uriProp , "Delimiter",Delimiter);
properties = trim(properties) !! " " !! trim(PropertyName) !! trim(Delimiter) !! trim(PropertyValue);
output;
k+1;
rcProp = metadata_getnasn(uriCon, "Properties", k, uriProp);
end;
%&mD.put NOTE: Getting properties for REMOTE SHARE &libref library;
&mD.put _all_;
%&mD.put NOTE: Libname cmd will be:;
%&mD.put libname &libref &engine &properties slibref=&libref;
call symputx ("properties",trim(properties),'l');
run;
libname &libref &engine &properties slibref=&libref;
%end;
%else %if &engine=OLEDB %then %do;
%&mD.put NOTE: Retrieving OLEDB connection details;
data _null_;
length domain datasource provider properties schema
connx_uri domain_uri conprop_uri lib_uri schema_uri value $256.;
call missing (of _all_);
/* get source connection ID */
rc=metadata_getnasn("&liburi",'LibraryConnection',1,connx_uri);
/* get connection domain */
rc1=metadata_getnasn(connx_uri,'Domain',1,domain_uri);
rc2=metadata_getattr(domain_uri,'Name',domain);
&mD.putlog / 'NOTE: ' // 'NOTE- connection id: ' connx_uri ;
&mD.putlog 'NOTE- domain: ' domain;
/* get DSN and PROVIDER from connection properties */
i=0;
do until (rc<0);
i+1;
rc=metadata_getnasn(connx_uri,'Properties',i,conprop_uri);
rc2=metadata_getattr(conprop_uri,'Name',value);
if value='Connection.OLE.Property.DATASOURCE.Name.xmlKey.txt' then do;
rc3=metadata_getattr(conprop_uri,'DefaultValue',datasource);
end;
else if value='Connection.OLE.Property.PROVIDER.Name.xmlKey.txt' then do;
rc4=metadata_getattr(conprop_uri,'DefaultValue',provider);
end;
else if value='Connection.OLE.Property.PROPERTIES.Name.xmlKey.txt' then do;
rc5=metadata_getattr(conprop_uri,'DefaultValue',properties);
end;
end;
&mD.putlog 'NOTE- dsn/provider/properties: ' /
datasource provider properties;
&mD.putlog 'NOTE- schema: ' schema // 'NOTE-';
/* get SCHEMA */
rc6=metadata_getnasn("&liburi",'UsingPackages',1,lib_uri);
rc7=metadata_getattr(lib_uri,'SchemaName',schema);
call symputx('SQL_domain',domain,'l');
call symputx('SQL_dsn',datasource,'l');
call symputx('SQL_provider',provider,'l');
call symputx('SQL_properties',properties,'l');
call symputx('SQL_schema',schema,'l');
run;
%if %length(&open_passthrough)>0 %then %do;
proc sql &sql_options;
connect to OLEDB as &open_passthrough(INSERT_SQL=YES
/* need additional properties to make this work */
properties=('Integrated Security'=SSPI
'Persist Security Info'=True
%sysfunc(compress(%str(&SQL_properties),%str(())))
)
DATASOURCE=&sql_dsn PROMPT=NO
PROVIDER=&sql_provider SCHEMA=&sql_schema CONNECTION = GLOBAL);
%end;
%else %do;
LIBNAME &libref OLEDB PROPERTIES=&sql_properties
DATASOURCE=&sql_dsn PROVIDER=&sql_provider SCHEMA=&sql_schema
%if %length(&sql_domain)>0 %then %do;
authdomain="&sql_domain"
%end;
connection=shared;
%end;
%end;
%else %if &engine=ODBC %then %do;
&mD.%put NOTE: Retrieving ODBC connection details;
data _null_;
length connx_uri conprop_uri value datasource up_uri schema $256.;
call missing (of _all_);
/* get source connection ID */
rc=metadata_getnasn("&liburi",'LibraryConnection',1,connx_uri);
/* get connection properties */
i=0;
do until (rc2<0);
i+1;
rc2=metadata_getnasn(connx_uri,'Properties',i,conprop_uri);
rc3=metadata_getattr(conprop_uri,'Name',value);
if value='Connection.ODBC.Property.DATASRC.Name.xmlKey.txt' then do;
rc4=metadata_getattr(conprop_uri,'DefaultValue',datasource);
rc2=-1;
end;
end;
/* get SCHEMA */
rc6=metadata_getnasn("&liburi",'UsingPackages',1,up_uri);
rc7=metadata_getattr(up_uri,'SchemaName',schema);
&mD.put rc= connx_uri= rc2= conprop_uri= rc3= value= rc4= datasource=
rc6= up_uri= rc7= schema=;
call symputx('SQL_schema',schema,'l');
call symputx('SQL_dsn',datasource,'l');
run;
%if %length(&open_passthrough)>0 %then %do;
proc sql &sql_options;
connect to ODBC as &open_passthrough
(INSERT_SQL=YES DATASRC=&sql_dsn. CONNECTION=global);
%end;
%else %do;
libname &libref ODBC DATASRC=&sql_dsn SCHEMA=&sql_schema;
%end;
%end;
%else %if &engine=POSTGRES %then %do;
%put NOTE: Obtaining POSTGRES library details;
data _null_;
length database ignore_read_only_columns direct_exe preserve_col_names
preserve_tab_names server schema authdomain user password
prop name value uri urisrc $256.;
call missing (of _all_);
/* get database value */
prop='Connection.DBMS.Property.DB.Name.xmlKey.txt';
rc=metadata_getprop("&liburi",prop,database,"");
if database^='' then database='database='!!quote(trim(database));
call symputx('database',database,'l');
/* get IGNORE_READ_ONLY_COLUMNS value */
prop='Library.DBMS.Property.DBIROC.Name.xmlKey.txt';
rc=metadata_getprop("&liburi",prop,ignore_read_only_columns,"");
if ignore_read_only_columns^='' then ignore_read_only_columns=
'ignore_read_only_columns='!!ignore_read_only_columns;
call symputx('ignore_read_only_columns',ignore_read_only_columns,'l');
/* get DIRECT_EXE value */
prop='Library.DBMS.Property.DirectExe.Name.xmlKey.txt';
rc=metadata_getprop("&liburi",prop,direct_exe,"");
if direct_exe^='' then direct_exe='direct_exe='!!direct_exe;
call symputx('direct_exe',direct_exe,'l');
/* get PRESERVE_COL_NAMES value */
prop='Library.DBMS.Property.PreserveColNames.Name.xmlKey.txt';
rc=metadata_getprop("&liburi",prop,preserve_col_names,"");
if preserve_col_names^='' then preserve_col_names=
'preserve_col_names='!!preserve_col_names;
call symputx('preserve_col_names',preserve_col_names,'l');
/* get PRESERVE_TAB_NAMES value */
/* be careful with PRESERVE_TAB_NAMES=YES - it will mean your table will
become case sensitive!! */
prop='Library.DBMS.Property.PreserveTabNames.Name.xmlKey.txt';
rc=metadata_getprop("&liburi",prop,preserve_tab_names,"");
if preserve_tab_names^='' then preserve_tab_names=
'preserve_tab_names='!!preserve_tab_names;
call symputx('preserve_tab_names',preserve_tab_names,'l');
/* get SERVER value */
if metadata_getnasn("&liburi","LibraryConnection",1,uri)>0 then do;
prop='Connection.DBMS.Property.SERVER.Name.xmlKey.txt';
rc=metadata_getprop(uri,prop,server,"");
end;
if server^='' then server='server='!!server;
call symputx('server',server,'l');
/* get SCHEMA value */
if metadata_getnasn("&liburi","UsingPackages",1,uri)>0 then do;
rc=metadata_getattr(uri,"SchemaName",schema);
end;
if schema^='' then schema='schema='!!schema;
call symputx('schema',schema,'l');
/* get AUTHDOMAIN value */
/* this is only useful if the user account contains that auth domain
if metadata_getnasn("&liburi","DefaultLogin",1,uri)>0 then do;
rc=metadata_getnasn(uri,"Domain",1,urisrc);
rc=metadata_getattr(urisrc,"Name",authdomain);
end;
if authdomain^='' then authdomain='authdomain='!!quote(trim(authdomain));
*/
call symputx('authdomain',authdomain,'l');
/* get user & pass */
if authdomain='' & metadata_getnasn("&liburi","DefaultLogin",1,uri)>0 then
do;
rc=metadata_getattr(uri,"UserID",user);
rc=metadata_getattr(uri,"Password",password);
end;
if user^='' then do;
user='user='!!quote(trim(user));
password='password='!!quote(trim(password));
end;
call symputx('user',user,'l');
call symputx('password',password,'l');
&md.put _all_;
run;
%if %length(&open_passthrough)>0 %then %do;
%put WARNING: Passthrough option for postgres not yet supported;
%return;
%end;
%else %do;
%if &mdebug=1 %then %do;
%put NOTE: Executing the following:/;
%put NOTE- libname &libref POSTGRES &database &ignore_read_only_columns;
%put NOTE- &direct_exe &preserve_col_names &preserve_tab_names;
%put NOTE- &server &schema &authdomain &user &password //;
%end;
libname &libref POSTGRES &database &ignore_read_only_columns &direct_exe
&preserve_col_names &preserve_tab_names &server &schema &authdomain
&user &password;
%end;
%end;
%else %if &engine=ORACLE %then %do;
%put NOTE: Obtaining &engine library details;
data _null_;
length assocuri1 assocuri2 assocuri3 authdomain path schema $256;
call missing (of _all_);
/* get auth domain */
rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri1);
rc=metadata_getnasn(assocuri1,'Domain',1,assocuri2);
rc=metadata_getattr(assocuri2,"Name",authdomain);
call symputx('authdomain',authdomain,'l');
/* path */
rc=metadata_getprop(assocuri1,'Connection.Oracle.Property.PATH.Name.xmlKey.txt',path);
call symputx('path',path,'l');
/* schema */
rc=metadata_getnasn("&liburi",'UsingPackages',1,assocuri3);
rc=metadata_getattr(assocuri3,'SchemaName',schema);
call symputx('schema',schema,'l');
run;
%put NOTE: Executing the following:/; %put NOTE-;
%put NOTE- libname &libref ORACLE path=&path schema=&schema authdomain=&authdomain;
%put NOTE-;
libname &libref ORACLE path=&path schema=&schema authdomain=&authdomain;
%end;
%else %if &engine=SQLSVR %then %do;
%put NOTE: Obtaining &engine library details;
data _null;
length assocuri1 assocuri2 assocuri3 authdomain path schema userid passwd $256;
call missing (of _all_);
rc=metadata_getnasn("&liburi",'DefaultLogin',1,assocuri1);
rc=metadata_getattr(assocuri1,"UserID",userid);
rc=metadata_getattr(assocuri1,"Password",passwd);
call symputx('user',userid,'l');
call symputx('pass',passwd,'l');
/* path */
rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri2);
rc=metadata_getprop(assocuri2,'Connection.SQL.Property.Datasrc.Name.xmlKey.txt',path);
call symputx('path',path,'l');
/* schema */
rc=metadata_getnasn("&liburi",'UsingPackages',1,assocuri3);
rc=metadata_getattr(assocuri3,'SchemaName',schema);
call symputx('schema',schema,'l');
run;
%put NOTE: Executing the following:/; %put NOTE-;
%put NOTE- libname &libref SQLSVR datasrc=&path schema=&schema user="&user" pass="XXX";
%put NOTE-;
libname &libref SQLSVR datasrc=&path schema=&schema user="&user" pass="&pass" ;
%end;
%else %if &engine=TERADATA %then %do;
%put NOTE: Obtaining &engine library details;
data _null;
length assocuri1 assocuri2 assocuri3 authdomain path schema userid passwd $256;
call missing (of _all_);
/* get auth domain */
rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri1);
rc=metadata_getnasn(assocuri1,'Domain',1,assocuri2);
rc=metadata_getattr(assocuri2,"Name",authdomain);
call symputx('authdomain',authdomain,'l');
/*
rc=metadata_getnasn("&liburi",'DefaultLogin',1,assocuri1);
rc=metadata_getattr(assocuri1,"UserID",userid);
rc=metadata_getattr(assocuri1,"Password",passwd);
call symputx('user',userid,'l');
call symputx('pass',passwd,'l');
*/
/* path */
rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri2);
rc=metadata_getprop(assocuri2,'Connection.Teradata.Property.SERVER.Name.xmlKey.txt',path);
call symputx('path',path,'l');
/* schema */
rc=metadata_getnasn("&liburi",'UsingPackages',1,assocuri3);
rc=metadata_getattr(assocuri3,'SchemaName',schema);
call symputx('schema',schema,'l');
run;
%put NOTE: Executing the following:/; %put NOTE-;
%put NOTE- libname &libref TERADATA server=&path schema=&schema authdomain=&authdomain;
%put NOTE-;
libname &libref TERADATA server=&path schema=&schema authdomain=&authdomain;
%end;
%else %if &engine= %then %do;
%put NOTE: Libref &libref is not registered in metadata;
%&mAbort.mp_abort(
msg=%str(ERR)OR: Libref &libref is not registered in metadata
,mac=mm_assigndirectlib.sas);
%return;
%end;
%else %do;
%put WARNING: Engine &engine is currently unsupported;
%put WARNING- Please contact your support team.;
%return;
%end;
%mend;

76
meta/mm_assignlib.sas Executable file
View File

@@ -0,0 +1,76 @@
/**
@file
@brief Assigns a meta engine library using LIBREF
@details Queries metadata to get the library NAME which can then be used in
a libname statement with the meta engine.
usage:
%macro mp_abort(iftrue,mac,msg);%put &=msg;%mend;
%mm_assignlib(SOMEREF)
<h4> Dependencies </h4>
@li mp_abort.sas
@param libref the libref (not name) of the metadata library
@param mAbort= If not assigned, HARD will call %mp_abort(), SOFT will silently return
@returns libname statement
@version 9.2
@author Allan Bowe
**/
%macro mm_assignlib(
libref
,mAbort=HARD
)/*/STORE SOURCE*/;
%if %sysfunc(libref(&libref)) %then %do;
%local mp_abort msg; %let mp_abort=0;
data _null_;
length liburi LibName $200;
call missing(of _all_);
nobj=metadata_getnobj("omsobj:SASLibrary?@Libref='&libref'",1,liburi);
if nobj=1 then do;
rc=metadata_getattr(liburi,"Name",LibName);
/* now try and assign it */
if libname("&libref",,'meta',cats('liburi="',liburi,'";')) ne 0 then do;
call symputx('msg',sysmsg(),'l');
if "&mabort"='HARD' then call symputx('mp_abort',1,'l');
end;
else do;
put (_all_)(=);
call symputx('libname',libname,'L');
call symputx('liburi',liburi,'L');
end;
end;
else if nobj>1 then do;
if "&mabort"='HARD' then call symputx('mp_abort',1);
call symputx('msg',"More than one library with libref=&libref");
end;
else do;
if "&mabort"='HARD' then call symputx('mp_abort',1);
call symputx('msg',"Library &libref not found in metadata");
end;
run;
%if &mp_abort=1 %then %do;
%mp_abort(iftrue= (&mp_abort=1)
,mac=&sysmacroname
,msg=&msg
)
%return;
%end;
%else %if %length(&msg)>2 %then %do;
%put NOTE: &msg;
%return;
%end;
%end;
%else %do;
%put NOTE: Library &libref is already assigned;
%end;
%mend;

View File

@@ -0,0 +1,153 @@
/**
@file
@brief Create an Application object in a metadata folder
@details Application objects are useful for storing properties in metadata.
This macro is idempotent - it will not create an object with the same name
in the same location, twice.
usage:
%mm_createapplication(tree=/User Folders/sasdemo
,name=MyApp
,classidentifier=myAppSeries
,params= name1=value1&#x0a;name2=value2&#x0a;emptyvalue=
)
@warning application components do not get deleted when removing the container folder! be sure you have the administrative priviliges to remove this kind of metadata from the SMC plugin (or be ready to do to so programmatically).
<h4> Dependencies </h4>
@li mp_abort.sas
@li mf_verifymacvars.sas
@param tree= The metadata folder uri, or the metadata path, in which to
create the object. This must exist.
@param name= Application object name. Avoid spaces.
@param ClassIdentifier= the class of applications to which this app belongs
@param params= name=value pairs which will become public properties of the
application object. These are delimited using &#x0a; (newline character)
@param desc= Application description (optional). Avoid ampersands as these
are illegal characters (unless they are escapted- eg &amp;)
@param version= version number of application
@param frefin= fileref to use (enables change if there is a conflict). The
filerefs are left open, to enable inspection after running the
macro (or importing into an xmlmap if needed).
@param frefout= fileref to use (enables change if there is a conflict)
@param mDebug= set to 1 to show debug messages in the log
@author Allan Bowe
**/
%macro mm_createapplication(
tree=/User Folders/sasdemo
,name=myApp
,ClassIdentifier=mcore
,desc=Created by mm_createapplication
,params= param1=1&#x0a;param2=blah
,version=
,frefin=mm_in
,frefout=mm_out
,mDebug=1
);
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing &sysmacroname..sas;
%&mD.put _local_;
%mf_verifymacvars(tree name)
/**
* check tree exists
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree","Folder",type,uri);
call symputx('type',type,'l');
call symputx('treeuri',uri,'l');
run;
%mp_abort(
iftrue= (&type ne Tree)
,mac=mm_createapplication.sas
,msg=Tree &tree does not exist!
)
/**
* Check object does not exist already
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree/&name","Application",type,uri);
call symputx('type',type,'l');
putlog (_all_)(=);
run;
%mp_abort(
iftrue= (&type = SoftwareComponent)
,mac=mm_createapplication.sas
,msg=Application &name already exists in &tree!
)
/**
* Now we can create the application
*/
filename &frefin temp;
/* write header XML */
data _null_;
file &frefin;
name=quote(symget('name'));
desc=quote(symget('desc'));
ClassIdentifier=quote(symget('ClassIdentifier'));
version=quote(symget('version'));
params=quote(symget('params'));
treeuri=quote(symget('treeuri'));
put "<AddMetadata><Reposid>$METAREPOSITORY</Reposid><Metadata> "/
'<SoftwareComponent IsHidden="0" Name=' name ' ProductName=' name /
' ClassIdentifier=' ClassIdentifier ' Desc=' desc /
' SoftwareVersion=' version ' SpecVersion=' version /
' Major="1" Minor="1" UsageVersion="1000000" PublicType="Application" >' /
' <Notes>' /
' <TextStore Name="Public Configuration Properties" IsHidden="0" ' /
' UsageVersion="0" StoredText=' params '/>' /
' </Notes>' /
"<Trees><Tree ObjRef=" treeuri "/></Trees>"/
"</SoftwareComponent></Metadata><NS>SAS</NS>"/
"<Flags>268435456</Flags></AddMetadata>";
run;
filename &frefout temp;
proc metadata in= &frefin out=&frefout verbose;
run;
%if &mdebug=1 %then %do;
/* write the response to the log for debugging */
data _null_;
infile &frefout lrecl=1048576;
input;
put _infile_;
run;
%end;
%put NOTE: Checking to ensure application (&name) was created;
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree/&name","Application",type,uri);
call symputx('apptype',type,'l');
%if &mdebug=1 %then putlog (_all_)(=);;
run;
%if &apptype ne SoftwareComponent %then %do;
%put %str(ERR)OR: Could not find (&name) at (&tree)!!;
%return;
%end;
%else %put NOTE: Application (&name) successfully created in (&tree)!;
%mend;

83
meta/mm_createdataset.sas Normal file
View File

@@ -0,0 +1,83 @@
/**
@file mm_createdataset.sas
@brief Create a dataset from a metadata definition
@details This macro was built to support viewing empty tables in
https://datacontroller.io - a free evaluation copy is available by
contacting the author (Allan Bowe).
The table can be retrieved using LIBRARY.DATASET reference, or directly
using the metadata URI.
The dataset is written to the WORK library.
usage:
%mm_createdataset(libds=metlib.some_dataset)
or
%mm_createdataset(tableuri=G5X8AFW1.BE00015Y)
<h4> Dependencies </h4>
@li mm_getlibs.sas
@li mm_gettables.sas
@li mm_getcols.sas
@param libds= library.dataset metadata source. Note - table names in metadata
can be longer than 32 chars (just fyi, not an issue here)
@param tableuri= Metadata URI of the table to be created
@param outds= The dataset to create, default is `work.mm_createdataset`.
The table name needs to be 32 chars or less as per SAS naming rules.
@param mdebug= set DBG to 1 to disable DEBUG messages
@version 9.4
@author Allan Bowe
**/
%macro mm_createdataset(libds=,tableuri=,outds=work.mm_createdataset,mDebug=0);
%local dbg errorcheck tempds1 tempds2 tempds3;
%if &mDebug=0 %then %let dbg=*;
%let errorcheck=1;
%if %index(&libds,.)>0 %then %do;
/* get lib uri */
data;run;%let tempds1=&syslast;
%mm_getlibs(outds=&tempds1)
data _null_;
set &tempds1;
if upcase(libraryref)="%upcase(%scan(&libds,1,.))";
call symputx('liburi',LibraryId,'l');
run;
/* get ds uri */
data;run;%let tempds2=&syslast;
%mm_gettables(uri=&liburi,outds=&tempds2)
data _null_;
set &tempds2;
if upcase(tablename)="%upcase(%scan(&libds,2,.))";
call symputx('tableuri',tableuri);
run;
%end;
data;run;%let tempds3=&syslast;
%mm_getcols(tableuri=&tableuri,outds=&tempds3)
data _null_;
set &tempds3 end=last;
if _n_=1 then call execute('data &outds;');
length attrib $32767;
if SAScolumntype='C' then type='$';
attrib='attrib '!!cats(colname)!!' length='!!cats(type,SASColumnLength,'.');
if not missing(sasformat) then fmt=' format='!!cats(sasformat);
if not missing(sasinformat) then infmt=' informat='!!cats(sasinformat);
if not missing(coldesc) then desc=' label='!!quote(cats(coldesc));
attrib=trim(attrib)!!fmt!!infmt!!desc!!';';
call execute(attrib);
if last then call execute('call missing(of _all_);stop;run;');
run;
%mend;

125
meta/mm_createdocument.sas Normal file
View File

@@ -0,0 +1,125 @@
/**
@file
@brief Create a Document object in a metadata folder
@details Document objects are useful for storing properties in metadata.
This macro is idempotent - it will not create an object with the same name
in the same location, twice.
Note - the filerefs are left open, to enable inspection after running the
macro (or importing into an xmlmap if needed).
usage:
%mm_createdocument(tree=/User Folders/sasdemo
,name=MyNote)
<h4> Dependencies </h4>
@li mp_abort.sas
@li mf_verifymacvars.sas
@param tree= The metadata folder uri, or the metadata path, in which to
create the document. This must exist.
@param name= Document object name. Avoid spaces.
@param desc= Document description (optional)
@param textrole= TextRole property (optional)
@param frefin= fileref to use (enables change if there is a conflict)
@param frefout= fileref to use (enables change if there is a conflict)
@param mDebug= set to 1 to show debug messages in the log
@author Allan Bowe
**/
%macro mm_createdocument(
tree=/User Folders/sasdemo
,name=myNote
,desc=Created by &sysmacroname
,textrole=
,frefin=mm_in
,frefout=mm_out
,mDebug=1
);
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing &sysmacroname..sas;
%&mD.put _local_;
%mf_verifymacvars(tree name)
/**
* check tree exists
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree","Folder",type,uri);
call symputx('type',type,'l');
call symputx('treeuri',uri,'l');
run;
%mp_abort(
iftrue= (&type ne Tree)
,mac=mm_createdocument.sas
,msg=Tree &tree does not exist!
)
/**
* Check object does not exist already
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree/&name","Note",type,uri);
call symputx('type',type,'l');
call symputx('docuri',uri,'l');
putlog (_all_)(=);
run;
%if &type = Document %then %do;
%put Document &name already exists in &tree!;
%return;
%end;
/**
* Now we can create the document
*/
filename &frefin temp;
/* write header XML */
data _null_;
file &frefin;
name=quote("&name");
desc=quote("&desc");
textrole=quote("&textrole");
treeuri=quote("&treeuri");
put "<AddMetadata><Reposid>$METAREPOSITORY</Reposid>"/
'<Metadata><Document IsHidden="0" PublicType="Note" UsageVersion="1000000"'/
" Name=" name " desc=" desc " TextRole=" textrole ">"/
"<Notes> "/
' <TextStore IsHidden="0" Name=' name ' UsageVersion="0" '/
' TextRole="SourceCode" StoredText="hello world" />' /
'</Notes>'/
/*URI="Document for public note" */
"<Trees><Tree ObjRef=" treeuri "/></Trees>"/
"</Document></Metadata><NS>SAS</NS>"/
"<Flags>268435456</Flags></AddMetadata>";
run;
filename &frefout temp;
proc metadata in= &frefin out=&frefout verbose;
run;
%if &mdebug=1 %then %do;
/* write the response to the log for debugging */
data _null_;
infile &frefout lrecl=1048576;
input;
put _infile_;
run;
%end;
%mend;

159
meta/mm_createfolder.sas Normal file
View File

@@ -0,0 +1,159 @@
/**
@file
@brief Recursively create a metadata folder
@details This macro was inspired by Paul Homes who wrote an early
version (mkdirmd.sas) in 2010. The original is described here:
https://platformadmin.com/blogs/paul/2010/07/mkdirmd/
The macro will NOT create a new ROOT folder - not
because it can't, but more because that is generally not something
your administrator would like you to do!
The macro is idempotent - if you run it twice, it will only create a folder
once.
usage:
%mm_createfolder(path=/some/meta/folder)
@param path= Name of the folder to create.
@param mdebug= set DBG to 1 to disable DEBUG messages
@version 9.4
@author Allan Bowe
**/
%macro mm_createfolder(path=,mDebug=0);
%put &sysmacroname: execution started for &path;
%local dbg errorcheck;
%if &mDebug=0 %then %let dbg=*;
%local parentFolderObjId child errorcheck paths;
%let paths=0;
%let errorcheck=1;
%if &syscc ge 4 %then %do;
%put SYSCC=&syscc - this macro requires a clean session;
%return;
%end;
data _null_;
length objId parentId objType parent child $200
folderPath $1000;
call missing (of _all_);
folderPath = "%trim(&path)";
* remove any trailing slash ;
if ( substr(folderPath,length(folderPath),1) = '/' ) then
folderPath=substr(folderPath,1,length(folderPath)-1);
* name must not be blank;
if ( folderPath = '' ) then do;
put "%str(ERR)OR: &sysmacroname PATH parameter value must be non-blank";
end;
* must have a starting slash ;
if ( substr(folderPath,1,1) ne '/' ) then do;
put "%str(ERR)OR: &sysmacroname PATH parameter value must have starting slash";
stop;
end;
* check if folder already exists ;
rc=metadata_pathobj('',cats(folderPath,"(Folder)"),"",objType,objId);
if rc ge 1 then do;
put "NOTE: Folder " folderPath " already exists!";
stop;
end;
* do not create a root (one level) folder ;
if countc(folderPath,'/')=1 then do;
put "%str(ERR)OR: &sysmacroname will not create a new ROOT folder";
stop;
end;
* check that root folder exists ;
root=cats('/',scan(folderpath,1,'/'),"(Folder)");
if metadata_pathobj('',root,"",objType,parentId)<1 then do;
put "%str(ERR)OR: " root " does not exist!";
stop;
end;
* check that parent folder exists ;
child=scan(folderPath,-1,'/');
parent=substr(folderpath,1,length(folderpath)-length(child)-1);
rc=metadata_pathobj('',cats(parent,"(Folder)"),"",objType,parentId);
if rc<1 then do;
putlog 'The following folders will be created:';
/* folder does not exist - so start from top and work down */
length newpath $1000;
paths=0;
do x=2 to countw(folderpath,'/');
newpath='';
do i=1 to x;
newpath=cats(newpath,'/',scan(folderpath,i,'/'));
end;
rc=metadata_pathobj('',cats(newpath,"(Folder)"),"",objType,parentId);
if rc<1 then do;
paths+1;
call symputx(cats('path',paths),newpath);
putlog newpath;
end;
call symputx('paths',paths);
end;
end;
else putlog "parent " parent " exists";
call symputx('parentFolderObjId',parentId,'l');
call symputx('child',child,'l');
call symputx('errorcheck',0,'l');
&dbg put (_all_)(=);
run;
%if &errorcheck=1 or &syscc ge 4 %then %return;
%if &paths>0 %then %do x=1 %to &paths;
%put executing recursive call for &&path&x;
%mm_createfolder(path=&&path&x)
%end;
%else %do;
filename __newdir temp;
options noquotelenmax;
%local inmeta;
%put creating: &path;
%let inmeta=<AddMetadata><Reposid>$METAREPOSITORY</Reposid><Metadata>
<Tree Name='&child' PublicType='Folder' TreeType='BIP Folder' UsageVersion='1000000'>
<ParentTree><Tree ObjRef='&parentFolderObjId'/></ParentTree></Tree></Metadata>
<NS>SAS</NS><Flags>268435456</Flags></AddMetadata>;
proc metadata in="&inmeta" out=__newdir verbose;
run ;
/* check it was successful */
data _null_;
length objId parentId objType parent child $200 ;
call missing (of _all_);
rc=metadata_pathobj('',cats("&path","(Folder)"),"",objType,objId);
if rc ge 1 then do;
putlog "SUCCCESS! &path created.";
end;
else do;
putlog "%str(ERR)OR: unsuccessful attempt to create &path";
call symputx('syscc',8);
end;
run;
/* write the response to the log for debugging */
%if &mDebug ne 0 %then %do;
data _null_;
infile __newdir lrecl=32767;
input;
put _infile_;
run;
%end;
filename __newdir clear;
%end;
%put &sysmacroname: execution finished for &path;
%mend;

321
meta/mm_createlibrary.sas Normal file
View File

@@ -0,0 +1,321 @@
/**
@file
@brief Create a SAS Library
@details Currently only supports BASE engine
This macro is idempotent - if you run it twice (for the same libref or
libname), it will only create one library. There is a dependency on other
macros in this library - they should be installed as a suite (see README).
Usage:
%mm_createlibrary(
libname=My New Library
,libref=mynewlib
,libdesc=Super & <fine>
,engine=BASE
,tree=/User Folders/sasdemo
,servercontext=SASApp
,directory=/tmp/tests
,mDebug=1)
<h4> Dependencies </h4>
@li mf_verifymacvars.sas
@li mm_createfolder.sas
@param libname= Library name (as displayed to user, 256 chars). Duplicates
are not created (case sensitive).
@param libref= Library libref (8 chars). Duplicate librefs are not created,
HOWEVER- the check is not case sensitive - if *libref* exists, *LIBREF*
will still be created. Librefs created will always be uppercased.
@param engine= Library engine (currently only BASE supported)
@param tree= The metadata folder uri, or the metadata path, in which to
create the library.
@param servercontext= The SAS server against which the library is registered.
@param IsPreassigned= set to 1 if the library should be pre-assigned.
@param libdesc= Library description (optional)
@param directory= Required for the BASE engine. The metadata directory objects
are searched to find an existing one with a matching physical path.
If more than one uri found with that path, then the first one will be used.
If no URI is found, a new directory object will be created. The physical
path will also be created, if it doesn't exist.
@param mDebug= set to 1 to show debug messages in the log
@param frefin= fileref to use (enables change if there is a conflict). The
filerefs are left open, to enable inspection after running the
macro (or importing into an xmlmap if needed).
@param frefout= fileref to use (enables change if there is a conflict)
@version 9.3
@author Allan Bowe
**/
%macro mm_createlibrary(
libname=My New Library
,libref=mynewlib
,libdesc=Created automatically using the mm_createlibrary macro
,engine=BASE
,tree=/User Folders/sasdemo
,servercontext=SASApp
,directory=/tmp/somelib
,IsPreassigned=0
,mDebug=0
,frefin=mm_in
,frefout=mm_out
)/*/STORE SOURCE*/;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing &sysmacroname..sas;
%&mD.put _local_;
%let libref=%upcase(&libref);
/**
* Check Library does not exist already with this libname
*/
data _null_;
length type uri $256;
rc=metadata_resolve("omsobj:SASLibrary?@Name='&libname'",type,uri);
call symputx('checktype',type,'l');
call symputx('liburi',uri,'l');
putlog (_all_)(=);
run;
%if &checktype = SASLibrary %then %do;
%put WARNING: Library (&liburi) already exists with libname (&libname) ;
%return;
%end;
/**
* Check Library does not exist already with this libref
*/
data _null_;
length type uri $256;
rc=metadata_resolve("omsobj:SASLibrary?@Libref='&libref'",type,uri);
call symputx('checktype',type,'l');
call symputx('liburi',uri,'l');
putlog (_all_)(=);
run;
%if &checktype = SASLibrary %then %do;
%put WARNING: Library (&liburi) already exists with libref (&libref) ;
%return;
%end;
/**
* Attempt to create tree
*/
%mm_createfolder(path=&tree)
/**
* check tree exists
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree","Folder",type,uri);
call symputx('foldertype',type,'l');
call symputx('treeuri',uri,'l');
run;
%if &foldertype ne Tree %then %do;
%put WARNING: Tree &tree does not exist!;
%return;
%end;
/**
* Create filerefs for proc metadata call
*/
filename &frefin temp;
filename &frefout temp;
%if &engine=BASE %then %do;
%mf_verifymacvars(libname libref engine servercontext tree)
/**
* Check that the ServerContext exists
*/
data _null_;
length type uri $256;
rc=metadata_resolve("omsobj:ServerContext?@Name='&ServerContext'",type,uri);
call symputx('checktype',type,'l');
call symputx('serveruri',uri,'l');
putlog (_all_)(=);
run;
%if &checktype ne ServerContext %then %do;
%put %str(ERR)OR: ServerContext (&ServerContext) does not exist!;
%return;
%end;
/**
* Get prototype info
*/
data _null_;
length type uri str $256;
str="omsobj:Prototype?@Name='Library.SAS.Prototype.Name.xmlKey.txt'";
rc=metadata_resolve(str,type,uri);
call symputx('checktype',type,'l');
call symputx('prototypeuri',uri,'l');
putlog (_all_)(=);
run;
%if &checktype ne Prototype %then %do;
%put %str(ERR)OR: Prototype (Library.SAS.Prototype.Name.xmlKey.txt) not found!;
%return;
%end;
/**
* Check that Physical location exists
*/
%if %sysfunc(fileexist(&directory))=0 %then %do;
%put %str(ERR)OR: Physical directory (&directory) does not appear to exist!;
%return;
%end;
/**
* Check that Directory Object exists in metadata
*/
data _null_;
length type uri $256;
rc=metadata_resolve("omsobj:Directory?@DirectoryRole='LibraryPath'"
!!" and @DirectoryName='&directory'",type,uri);
call symputx('checktype',type,'l');
call symputx('directoryuri',uri,'l');
putlog (_all_)(=);
run;
%if &checktype ne Directory %then %do;
%put NOTE: Directory object does not exist for (&directory) location;
%put NOTE: It will now be created;
data _null_;
file &frefin;
directory=quote(symget('directory'));
put "<AddMetadata><Reposid>$METAREPOSITORY</Reposid><Metadata> "/
'<Directory UsageVersion="1000000" IsHidden="0" IsRelative="0"'/
' DirectoryRole="LibraryPath" Name="Path" DirectoryName=' directory '/>'/
"</Metadata><NS>SAS</NS>"/
"<Flags>268435456</Flags></AddMetadata>";
run;
proc metadata in= &frefin out=&frefout %if &mdebug=1 %then verbose;;
run;
%if &mdebug=1 %then %do;
data _null_;
infile &frefout lrecl=1048576;
input; put _infile_;
run;
%end;
%put NOTE: Checking to ensure directory (&directory) object was created;
data _null_;
length type uri $256;
rc=metadata_resolve("omsobj:Directory?@DirectoryRole='LibraryPath'"
!!" and @DirectoryName='&directory'",type,uri);
call symputx('checktype2',type,'l');
call symputx('directoryuri',uri,'l');
%if &mdebug=1 %then putlog (_all_)(=);;
run;
%if &checktype2 ne Directory %then %do;
%put %str(ERR)OR: Directory (&directory) object was NOT created!;
%return;
%end;
%else %put NOTE: Directory (&directoryuri) successfully created!;
%end;
/**
* check SAS version
*/
%if %sysevalf(&sysver lt 9.3) %then %do;
%put WARNING: Version 9.3 or later required;
%return;
%end;
/**
* Prepare the XML and create the library
*/
data _null_;
file &frefin;
treeuri=quote(symget('treeuri'));
serveruri=quote(symget('serveruri'));
directoryuri=quote(symget('directoryuri'));
libname=quote(symget('libname'));
libref=quote(symget('libref'));
IsPreassigned=quote(symget('IsPreassigned'));
prototypeuri=quote(symget('prototypeuri'));
/* escape description so it can be stored as XML */
libdesc=tranwrd(symget('libdesc'),'&','&amp;');
libdesc=tranwrd(libdesc,'<','&lt;');
libdesc=tranwrd(libdesc,'>','&gt;');
libdesc=tranwrd(libdesc,"'",'&apos;');
libdesc=tranwrd(libdesc,'"','&quot;');
libdesc=tranwrd(libdesc,'0A'x,'&#10;');
libdesc=tranwrd(libdesc,'0D'x,'&#13;');
libdesc=tranwrd(libdesc,'$','&#36;');
libdesc=quote(trim(libdesc));
put "<AddMetadata><Reposid>$METAREPOSITORY</Reposid><Metadata> "/
'<SASLibrary Desc=' libdesc ' Engine="BASE" IsDBMSLibname="0" '/
' IsHidden="0" IsPreassigned=' IsPreassigned ' Libref=' libref /
' UsageVersion="1000000" PublicType="Library" name=' libname '>'/
' <DeployedComponents>'/
' <ServerContext ObjRef=' serveruri "/>"/
' </DeployedComponents>'/
' <PropertySets>'/
' <PropertySet Name="ModifiedByProductPropertySet" '/
' SetRole="ModifiedByProductPropertySet" UsageVersion="0" />'/
' </PropertySets>'/
" <Trees><Tree ObjRef=" treeuri "/></Trees>"/
' <UsingPackages> '/
' <Directory ObjRef=' directoryuri ' />'/
' </UsingPackages>'/
' <UsingPrototype>'/
' <Prototype ObjRef=' prototypeuri '/>'/
' </UsingPrototype>'/
'</SASLibrary></Metadata><NS>SAS</NS>'/
'<Flags>268435456</Flags></AddMetadata>';
run;
proc metadata in= &frefin out=&frefout %if &mdebug=1 %then verbose ;;
run;
%if &mdebug=1 %then %do;
data _null_;
infile &frefout lrecl=1048576;
input;put _infile_;
run;
%end;
%put NOTE: Checking to ensure library (&libname) was created;
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree/&libname","Library",type,uri);
call symputx('libtype',type,'l');
call symputx('liburi',uri,'l');
%if &mdebug=1 %then putlog (_all_)(=);;
run;
%if &libtype ne SASLibrary %then %do;
%put %str(ERR)OR: Could not find (&libname) at (&tree)!!;
%return;
%end;
%else %put NOTE: Library (&libname) successfully created in (&tree)!;
%end;
%else %do;
%put %str(ERR)OR: Other library engine types are not yet supported!!;
%end;
/**
* Wrap up
*/
%if &mdebug ne 1 %then %do;
filename &frefin clear;
filename &frefout clear;
%end;
%mend;

389
meta/mm_createstp.sas Executable file
View File

@@ -0,0 +1,389 @@
/**
@file
@brief Create a type 1 Stored Process (9.2 compatible)
@details This macro creates a Type 1 stored process, and also the necessary
PromptGroup / File / TextStore objects. It requires the location (or uri)
for the App Server / Directory / Folder (Tree) objects.
To upgrade this macro to work with type 2 (which can embed SAS code
and is compabitible with SAS from 9.3 onwards) then the UsageVersion should
change to 2000000 and the TextStore object updated. The ComputeServer
reference will also be to ServerContext rather than LogicalServer.
This macro is idempotent - if you run it twice, it will only create an STP
once.
usage (type 1 STP):
%mm_createstp(stpname=MyNewSTP
,filename=mySpecialProgram.sas
,directory=SASEnvironment/SASCode/STPs
,tree=/User Folders/sasdemo
,outds=work.uris)
If you wish to remove the new STP you can do so by running:
data _null_;
set work.uris;
rc1 = METADATA_DELOBJ(texturi);
rc2 = METADATA_DELOBJ(prompturi);
rc3 = METADATA_DELOBJ(fileuri);
rc4 = METADATA_DELOBJ(stpuri);
putlog (_all_)(=);
run;
usage (type 2 STP):
%mm_createstp(stpname=MyNewType2STP
,filename=mySpecialProgram.sas
,directory=SASEnvironment/SASCode/STPs
,tree=/User Folders/sasdemo
,Server=SASApp
,stptype=2)
<h4> Dependencies </h4>
@li mf_nobs.sas
@li mf_verifymacvars.sas
@li mm_getdirectories.sas
@li mm_updatestpsourcecode.sas
@li mp_dropmembers.sas
@li mm_getservercontexts.sas
@param stpname= Stored Process name. Avoid spaces - testing has shown that
the check to avoid creating multiple STPs in the same folder with the same
name does not work when the name contains spaces.
@param stpdesc= Stored Process description (optional)
@param filename= the name of the .sas program to run
@param directory= The directory uri, or the actual path to the sas program
(no trailing slash). If more than uri is found with that path, then the
first one will be used.
@param tree= The metadata folder uri, or the metadata path, in which to
create the STP.
@param server= The server which will run the STP. Server name or uri is fine.
@param outds= The two level name of the output dataset. Will contain all the
meta uris. Defaults to work.mm_createstp.
@param mDebug= set to 1 to show debug messages in the log
@param stptype= Default is 1 (STP code saved on filesystem). Set to 2 if
source code is to be saved in metadata (9.3 and above feature).
@param minify= set to YES to strip comments / blank lines etc
@param frefin= fileref to use (enables change if there is a conflict). The
filerefs are left open, to enable inspection after running the
macro (or importing into an xmlmap if needed).
@param frefout= fileref to use (enables change if there is a conflict)
@param repo= ServerContext is tied to a repo, if you are not using the
foundation repo then select a different one here
@returns outds dataset containing the following columns:
- stpuri
- prompturi
- fileuri
- texturi
@version 9.2
@author Allan Bowe
**/
%macro mm_createstp(
stpname=Macro People STP
,stpdesc=This stp was created automatically by the mm_createstp macro
,filename=mm_createstp.sas
,directory=SASEnvironment/SASCode
,tree=/User Folders/sasdemo
,package=false
,streaming=true
,outds=work.mm_createstp
,mDebug=0
,server=SASApp
,stptype=1
,minify=NO
,frefin=mm_in
,frefout=mm_out
)/*/STORE SOURCE*/;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing mm_CreateSTP.sas;
%&mD.put _local_;
%mf_verifymacvars(stpname filename directory tree)
%mp_dropmembers(%scan(&outds,2,.))
/**
* check tree exists
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree","Folder",type,uri);
call symputx('foldertype',type,'l');
call symputx('treeuri',uri,'l');
run;
%if &foldertype ne Tree %then %do;
%put WARNING: Tree &tree does not exist!;
%return;
%end;
/**
* Check STP does not exist already
*/
%local cmtype;
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree/&stpname",'StoredProcess',type,uri);
call symputx('cmtype',type,'l');
call symputx('stpuri',uri,'l');
run;
%if &cmtype = ClassifierMap %then %do;
%put WARNING: Stored Process &stpname already exists in &tree!;
%return;
%end;
/**
* Check that the physical file exists
*/
%if %sysfunc(fileexist(&directory/&filename)) ne 1 %then %do;
%put WARNING: FILE *&directory/&filename* NOT FOUND!;
%return;
%end;
%if &stptype=1 %then %do;
/* type 1 STP - where code is stored on filesystem */
%if %sysevalf(&sysver lt 9.2) %then %do;
%put WARNING: Version 9.2 or later required;
%return;
%end;
/* check directory object (where 9.2 source code reference is stored) */
data _null_;
length id $20 dirtype $256;
rc=metadata_resolve("&directory",dirtype,id);
call symputx('checkdirtype',dirtype,'l');
run;
%if &checkdirtype ne Directory %then %do;
%mm_getdirectories(path=&directory,outds=&outds ,mDebug=&mDebug)
%if %mf_nobs(&outds)=0 or %sysfunc(exist(&outds))=0 %then %do;
%put WARNING: The directory object does not exist for &directory;
%return;
%end;
%end;
%else %do;
data &outds;
directoryuri="&directory";
run;
%end;
data &outds (keep=stpuri prompturi fileuri texturi);
length stpuri prompturi fileuri texturi serveruri $256 ;
set &outds;
/* final checks on uris */
length id $20 type $256;
__rc=metadata_resolve("&treeuri",type,id);
if type ne 'Tree' then do;
putlog "WARNING: Invalid tree URI: &treeuri";
stopme=1;
end;
__rc=metadata_resolve(directoryuri,type,id);
if type ne 'Directory' then do;
putlog 'WARNING: Invalid directory URI: ' directoryuri;
stopme=1;
end;
/* get server info */
__rc=metadata_resolve("&server",type,serveruri);
if type ne 'LogicalServer' then do;
__rc=metadata_getnobj("omsobj:LogicalServer?@Name='&server'",1,serveruri);
if serveruri='' then do;
putlog "WARNING: Invalid server: &server";
stopme=1;
end;
end;
if stopme=1 then do;
putlog (_all_)(=);
stop;
end;
/* create empty prompt */
rc1=METADATA_NEWOBJ('PromptGroup',prompturi,'Parameters');
rc2=METADATA_SETATTR(prompturi, 'UsageVersion', '1000000');
rc3=METADATA_SETATTR(prompturi, 'GroupType','2');
rc4=METADATA_SETATTR(prompturi, 'Name','Parameters');
rc5=METADATA_SETATTR(prompturi, 'PublicType','Embedded:PromptGroup');
GroupInfo="<PromptGroup promptId='PromptGroup_%sysfunc(datetime())_&sysprocessid'"
!!" version='1.0'><Label><Text xml:lang='en-GB'>Parameters</Text>"
!!"</Label></PromptGroup>";
rc6 = METADATA_SETATTR(prompturi, 'GroupInfo',groupinfo);
if sum(of rc1-rc6) ne 0 then do;
putlog 'WARNING: Issue creating prompt.';
if prompturi ne . then do;
putlog ' Removing orphan: ' prompturi;
rc = METADATA_DELOBJ(prompturi);
put rc=;
end;
stop;
end;
/* create a file uri */
rc7=METADATA_NEWOBJ('File',fileuri,'SP Source File');
rc8=METADATA_SETATTR(fileuri, 'FileName',"&filename");
rc9=METADATA_SETATTR(fileuri, 'IsARelativeName','1');
rc10=METADATA_SETASSN(fileuri, 'Directories','MODIFY',directoryuri);
if sum(of rc7-rc10) ne 0 then do;
putlog 'WARNING: Issue creating file.';
if fileuri ne . then do;
putlog ' Removing orphans:' prompturi fileuri;
rc = METADATA_DELOBJ(prompturi);
rc = METADATA_DELOBJ(fileuri);
put (_all_)(=);
end;
stop;
end;
/* create a TextStore object */
rc11= METADATA_NEWOBJ('TextStore',texturi,'Stored Process');
rc12= METADATA_SETATTR(texturi, 'TextRole','StoredProcessConfiguration');
rc13= METADATA_SETATTR(texturi, 'TextType','XML');
storedtext='<?xml version="1.0" encoding="UTF-8"?><StoredProcess>'
!!"<ResultCapabilities Package='&package' Streaming='&streaming'/>"
!!"<OutputParameters/></StoredProcess>";
rc14= METADATA_SETATTR(texturi, 'StoredText',storedtext);
if sum(of rc11-rc14) ne 0 then do;
putlog 'WARNING: Issue creating TextStore.';
if texturi ne . then do;
putlog ' Removing orphans: ' prompturi fileuri texturi;
rc = METADATA_DELOBJ(prompturi);
rc = METADATA_DELOBJ(fileuri);
rc = METADATA_DELOBJ(texturi);
put (_all_)(=);
end;
stop;
end;
/* create meta obj */
rc15= METADATA_NEWOBJ('ClassifierMap',stpuri,"&stpname");
rc16= METADATA_SETASSN(stpuri, 'Trees','MODIFY',treeuri);
rc17= METADATA_SETASSN(stpuri, 'ComputeLocations','MODIFY',serveruri);
rc18= METADATA_SETASSN(stpuri, 'SourceCode','MODIFY',fileuri);
rc19= METADATA_SETASSN(stpuri, 'Prompts','MODIFY',prompturi);
rc20= METADATA_SETASSN(stpuri, 'Notes','MODIFY',texturi);
rc21= METADATA_SETATTR(stpuri, 'PublicType', 'StoredProcess');
rc22= METADATA_SETATTR(stpuri, 'TransformRole', 'StoredProcess');
rc23= METADATA_SETATTR(stpuri, 'UsageVersion', '1000000');
rc24= METADATA_SETATTR(stpuri, 'Desc', "&stpdesc");
/* tidy up if err */
if sum(of rc15-rc24) ne 0 then do;
putlog "%str(WARN)ING: Issue creating STP.";
if stpuri ne . then do;
putlog ' Removing orphans: ' prompturi fileuri texturi stpuri;
rc = METADATA_DELOBJ(prompturi);
rc = METADATA_DELOBJ(fileuri);
rc = METADATA_DELOBJ(texturi);
rc = METADATA_DELOBJ(stpuri);
put (_all_)(=);
end;
end;
else do;
fullpath=cats('_program=',treepath,"/&stpname");
putlog "NOTE: Stored Process Created!";
putlog "NOTE- "; putlog "NOTE-"; putlog "NOTE-" fullpath;
putlog "NOTE- "; putlog "NOTE-";
end;
output;
stop;
run;
%end;
%else %if &stptype=2 %then %do;
/* type 2 stp - code is stored in metadata */
%if %sysevalf(&sysver lt 9.3) %then %do;
%put WARNING: SAS version 9.3 or later required to create type2 STPs;
%return;
%end;
/* check we have the correct ServerContext */
%mm_getservercontexts(outds=contexts)
%local serveruri; %let serveruri=NOTFOUND;
data _null_;
set contexts;
where upcase(servername)="%upcase(&server)";
call symputx('serveruri',serveruri);
run;
%if &serveruri=NOTFOUND %then %do;
%put WARNING: ServerContext *&server* not found!;
%return;
%end;
/**
* First, create a Hello World type 2 stored process
*/
filename &frefin temp;
data _null_;
file &frefin;
treeuri=quote(symget('treeuri'));
serveruri=quote(symget('serveruri'));
stpdesc=quote(symget('stpdesc'));
stpname=quote(symget('stpname'));
put "<AddMetadata><Reposid>$METAREPOSITORY</Reposid><Metadata> "/
'<ClassifierMap UsageVersion="2000000" IsHidden="0" IsUserDefined="0" '/
' IsActive="1" PublicType="StoredProcess" TransformRole="StoredProcess" '/
' Name=' stpname ' Desc=' stpdesc '>'/
" <ComputeLocations>"/
" <ServerContext ObjRef=" serveruri "/>"/
" </ComputeLocations>"/
"<Notes> "/
' <TextStore IsHidden="0" Name="SourceCode" UsageVersion="0" '/
' TextRole="StoredProcessSourceCode" StoredText="%put hello world!;" />'/
' <TextStore IsHidden="0" Name="Stored Process" UsageVersion="0" '/
' TextRole="StoredProcessConfiguration" TextType="XML" '/
' StoredText="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&qu'@@
'ot;?&gt;&lt;StoredProcess&gt;&lt;ServerContext LogicalServerType=&quot;S'@@
'ps&quot; OtherAllowed=&quot;false&quot;/&gt;&lt;ResultCapabilities Packa'@@
'ge=&quot;' @@ "&package" @@ '&quot; Streaming=&quot;' @@ "&streaming" @@
'&quot;/&gt;&lt;OutputParameters/&gt;&lt;/StoredProcess&gt;" />' /
" </Notes> "/
" <Prompts> "/
' <PromptGroup Name="Parameters" GroupType="2" IsHidden="0" '/
' PublicType="Embedded:PromptGroup" UsageVersion="1000000" '/
' GroupInfo="&lt;PromptGroup promptId=&quot;PromptGroup_1502797359253'@@
'_802080&quot; version=&quot;1.0&quot;&gt;&lt;Label&gt;&lt;Text xml:lang='@@
'&quot;en-US&quot;&gt;Parameters&lt;/Text&gt;&lt;/Label&gt;&lt;/PromptGro'@@
'up&gt;" />'/
" </Prompts> "/
"<Trees><Tree ObjRef=" treeuri "/></Trees>"/
"</ClassifierMap></Metadata><NS>SAS</NS>"/
"<Flags>268435456</Flags></AddMetadata>";
run;
filename &frefout temp;
proc metadata in= &frefin out=&frefout ;
run;
%if &mdebug=1 %then %do;
/* write the response to the log for debugging */
data _null_;
infile &frefout lrecl=1048576;
input;
put _infile_;
run;
%end;
/**
* Next, add the source code
*/
%mm_updatestpsourcecode(stp=&tree/&stpname
,stpcode="&directory/&filename"
,frefin=&frefin.
,frefout=&frefout.
,mdebug=&mdebug
,minify=&minify)
%end;
%else %do;
%put WARNING: STPTYPE=*&stptype* not recognised!;
%end;
%mend;

View File

@@ -0,0 +1,406 @@
/**
@file mm_createwebservice.sas
@brief Create a Web Ready Stored Process
@details This macro creates a Type 2 Stored Process with the macropeople
mm_webout macro included as pre-code.
Usage:
%* compile macros ;
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
%* parmcards lets us write to a text file from open code ;
filename ft15f001 temp;
parmcards4;
%* do some sas, any inputs are now already WORK tables;
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)
;;;;
%mm_createwebservice(path=/Public/app/common,name=appInit,code=ft15f001,replace=YES)
<h4> Dependencies </h4>
@li mm_createstp.sas
@li mf_getuser.sas
@li mm_createfolder.sas
@li mm_deletestp.sas
@param path= The full path (in SAS Metadata) where the service will be created
@param name= Stored Process name. Avoid spaces - testing has shown that
the check to avoid creating multiple STPs in the same folder with the same
name does not work when the name contains 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 server= The server which will run the STP. Server name or uri is fine.
@param mDebug= set to 1 to show debug messages in the log
@param replace= select YES to replace any existing service in that location
@param adapter= the macro uses the sasjs adapter by default. To use another
adapter, add a (different) fileref here.
@version 9.2
@author Allan Bowe
**/
%macro mm_createwebservice(path=
,name=initService
,precode=
,code=
,desc=This stp was created automagically by the mm_createwebservice macro
,mDebug=0
,server=SASApp
,replace=NO
,adapter=sasjs
)/*/STORE SOURCE*/;
%if &syscc ge 4 %then %do;
%put &=syscc - &sysmacroname will not execute in this state;
%return;
%end;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing mm_createwebservice.sas;
%&mD.put _local_;
* remove any trailing slash ;
%if "%substr(&path,%length(&path),1)" = "/" %then
%let path=%substr(&path,1,%length(&path)-1);
/**
* Add webout macro
* These put statements are auto generated - to change the macro, change the
* source (mm_webout) and run `build.py`
*/
filename sasjs temp;
data _null_;
file sasjs lrecl=3000 ;
put "/* Created on %sysfunc(datetime(),datetime19.) by %mf_getuser() */";
/* WEBOUT BEGIN */
put ' ';
put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=PROCJSON,dbg=0 ';
put ')/*/STORE SOURCE*/; ';
put '%put output location=&jref; ';
put '%if &action=OPEN %then %do; ';
put ' data _null_;file &jref encoding=''utf-8''; ';
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
put ' run; ';
put '%end; ';
put '%else %if (&action=ARR or &action=OBJ) %then %do; ';
put ' options validvarname=upcase; ';
put ' data _null_;file &jref mod encoding=''utf-8''; ';
put ' put ", ""%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":"; ';
put ' ';
put ' %if &engine=PROCJSON %then %do; ';
put ' data;run;%let tempds=&syslast; ';
put ' proc sql;drop table &tempds; ';
put ' data &tempds /view=&tempds;set &ds; ';
put ' %if &fmt=N %then format _numeric_ best32.;; ';
put ' proc json out=&jref ';
put ' %if &action=ARR %then nokeys ; ';
put ' %if &dbg ge 131 %then pretty ; ';
put ' ;export &tempds / nosastags fmtnumeric; ';
put ' run; ';
put ' proc sql;drop view &tempds; ';
put ' %end; ';
put ' %else %if &engine=DATASTEP %then %do; ';
put ' %local cols i tempds; ';
put ' %let cols=0; ';
put ' %if %sysfunc(exist(&ds)) ne 1 & %sysfunc(exist(&ds,VIEW)) ne 1 %then %do; ';
put ' %put &sysmacroname: &ds NOT FOUND!!!; ';
put ' %return; ';
put ' %end; ';
put ' data _null_;file &jref mod ; ';
put ' put "["; call symputx(''cols'',0,''l''); ';
put ' proc sort data=sashelp.vcolumn(where=(libname=''WORK'' & memname="%upcase(&ds)")) ';
put ' out=_data_; ';
put ' by varnum; ';
put ' ';
put ' data _null_; ';
put ' set _last_ end=last; ';
put ' call symputx(cats(''name'',_n_),name,''l''); ';
put ' call symputx(cats(''type'',_n_),type,''l''); ';
put ' call symputx(cats(''len'',_n_),length,''l''); ';
put ' if last then call symputx(''cols'',_n_,''l''); ';
put ' run; ';
put ' ';
put ' proc format; /* credit yabwon for special null removal */ ';
put ' value bart ._ - .z = null ';
put ' other = [best.]; ';
put ' ';
put ' data;run; %let tempds=&syslast; /* temp table for spesh char management */ ';
put ' proc sql; drop table &tempds; ';
put ' data &tempds/view=&tempds; ';
put ' attrib _all_ label=''''; ';
put ' %do i=1 %to &cols; ';
put ' %if &&type&i=char %then %do; ';
put ' length &&name&i $32767; ';
put ' format &&name&i $32767.; ';
put ' %end; ';
put ' %end; ';
put ' set &ds; ';
put ' format _numeric_ bart.; ';
put ' %do i=1 %to &cols; ';
put ' %if &&type&i=char %then %do; ';
put ' &&name&i=''"''!!trim(prxchange(''s/"/\"/'',-1, ';
put ' prxchange(''s/''!!''0A''x!!''/\n/'',-1, ';
put ' prxchange(''s/''!!''0D''x!!''/\r/'',-1, ';
put ' prxchange(''s/''!!''09''x!!''/\t/'',-1, ';
put ' prxchange(''s/\\/\\\\/'',-1,&&name&i) ';
put ' )))))!!''"''; ';
put ' %end; ';
put ' %end; ';
put ' run; ';
put ' /* write to temp loc to avoid _webout truncation - https://support.sas.com/kb/49/325.html */ ';
put ' filename _sjs temp lrecl=131068 encoding=''utf-8''; ';
put ' data _null_; file _sjs lrecl=131068 encoding=''utf-8'' mod; ';
put ' set &tempds; ';
put ' if _n_>1 then put "," @; put ';
put ' %if &action=ARR %then "[" ; %else "{" ; ';
put ' %do i=1 %to &cols; ';
put ' %if &i>1 %then "," ; ';
put ' %if &action=OBJ %then """&&name&i"":" ; ';
put ' &&name&i ';
put ' %end; ';
put ' %if &action=ARR %then "]" ; %else "}" ; ; ';
put ' proc sql; ';
put ' drop view &tempds; ';
put ' /* now write the long strings to _webout 1 byte at a time */ ';
put ' data _null_; ';
put ' length filein 8 fileid 8; ';
put ' filein = fopen("_sjs",''I'',1,''B''); ';
put ' fileid = fopen("&jref",''A'',1,''B''); ';
put ' rec = ''20''x; ';
put ' do while(fread(filein)=0); ';
put ' rc = fget(filein,rec,1); ';
put ' rc = fput(fileid, rec); ';
put ' rc =fwrite(fileid); ';
put ' end; ';
put ' rc = fclose(filein); ';
put ' rc = fclose(fileid); ';
put ' run; ';
put ' filename _sjs clear; ';
put ' data _null_; file &jref mod encoding=''utf-8''; ';
put ' put "]"; ';
put ' run; ';
put ' %end; ';
put '%end; ';
put ' ';
put '%else %if &action=CLOSE %then %do; ';
put ' data _null_;file &jref encoding=''utf-8''; ';
put ' put "}"; ';
put ' run; ';
put '%end; ';
put '%mend; ';
put '%macro mm_webout(action,ds,dslabel=,fref=_webout,fmt=Y); ';
put '%global _webin_file_count _webin_fileref1 _webin_name1 _program _debug; ';
put '%local i tempds; ';
put ' ';
put '%if &action=FETCH %then %do; ';
put ' %if %str(&_debug) ge 131 %then %do; ';
put ' options mprint notes mprintnest; ';
put ' %end; ';
put ' %let _webin_file_count=%eval(&_webin_file_count+0); ';
put ' /* now read in the data */ ';
put ' %do i=1 %to &_webin_file_count; ';
put ' %if &_webin_file_count=1 %then %do; ';
put ' %let _webin_fileref1=&_webin_fileref; ';
put ' %let _webin_name1=&_webin_name; ';
put ' %end; ';
put ' data _null_; ';
put ' infile &&_webin_fileref&i termstr=crlf; ';
put ' input; ';
put ' call symputx(''input_statement'',_infile_); ';
put ' putlog "&&_webin_name&i input statement: " _infile_; ';
put ' stop; ';
put ' data &&_webin_name&i; ';
put ' infile &&_webin_fileref&i firstobs=2 dsd termstr=crlf encoding=''utf-8''; ';
put ' input &input_statement; ';
put ' %if %str(&_debug) ge 131 %then %do; ';
put ' if _n_<20 then putlog _infile_; ';
put ' %end; ';
put ' run; ';
put ' %end; ';
put '%end; ';
put ' ';
put '%else %if &action=OPEN %then %do; ';
put ' /* fix encoding */ ';
put ' OPTIONS NOBOMFILE; ';
put ' data _null_; ';
put ' rc = stpsrv_header(''Content-type'',"text/html; encoding=utf-8"); ';
put ' run; ';
put ' ';
put ' /* setup json */ ';
put ' data _null_;file &fref encoding=''utf-8''; ';
put ' %if %str(&_debug) ge 131 %then %do; ';
put ' put ''>>weboutBEGIN<<''; ';
put ' %end; ';
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
put ' run; ';
put ' ';
put '%end; ';
put ' ';
put '%else %if &action=ARR or &action=OBJ %then %do; ';
put ' %if &sysver=9.4 %then %do; ';
put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt ';
put ' ,engine=PROCJSON,dbg=%str(&_debug) ';
put ' ) ';
put ' %end; ';
put ' %else %do; ';
put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt ';
put ' ,engine=DATASTEP,dbg=%str(&_debug) ';
put ' ) ';
put ' %end; ';
put '%end; ';
put '%else %if &action=CLOSE %then %do; ';
put ' %if %str(&_debug) ge 131 %then %do; ';
put ' /* if debug mode, send back first 10 records of each work table also */ ';
put ' options obs=10; ';
put ' data;run;%let tempds=%scan(&syslast,2,.); ';
put ' ods output Members=&tempds; ';
put ' proc datasets library=WORK memtype=data; ';
put ' %local wtcnt;%let wtcnt=0; ';
put ' data _null_; ';
put ' set &tempds; ';
put ' if not (name =:"DATA"); ';
put ' i+1; ';
put ' call symputx(''wt''!!left(i),name,''l''); ';
put ' call symputx(''wtcnt'',i,''l''); ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' put ",""WORK"":{"; ';
put ' %do i=1 %to &wtcnt; ';
put ' %let wt=&&wt&i; ';
put ' proc contents noprint data=&wt ';
put ' out=_data_ (keep=name type length format:); ';
put ' run;%let tempds=%scan(&syslast,2,.); ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' dsid=open("WORK.&wt",''is''); ';
put ' nlobs=attrn(dsid,''NLOBS''); ';
put ' nvars=attrn(dsid,''NVARS''); ';
put ' rc=close(dsid); ';
put ' if &i>1 then put '',''@; ';
put ' put " ""&wt"" : {"; ';
put ' put ''"nlobs":'' nlobs; ';
put ' put '',"nvars":'' nvars; ';
put ' %mp_jsonout(OBJ,&tempds,jref=&fref,dslabel=colattrs,engine=DATASTEP) ';
put ' %mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,engine=DATASTEP) ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' put "}"; ';
put ' %end; ';
put ' data _null_; file &fref encoding=''utf-8''; ';
put ' put "}"; ';
put ' run; ';
put ' %end; ';
put ' /* close off json */ ';
put ' data _null_;file &fref mod encoding=''utf-8''; ';
put ' _PROGRAM=quote(trim(resolve(symget(''_PROGRAM'')))); ';
put ' put ",""SYSUSERID"" : ""&sysuserid"" "; ';
put ' put ",""MF_GETUSER"" : ""%mf_getuser()"" "; ';
put ' put ",""_DEBUG"" : ""&_debug"" "; ';
put ' _METAUSER=quote(trim(symget(''_METAUSER''))); ';
put ' put ",""_METAUSER"": " _METAUSER; ';
put ' _METAPERSON=quote(trim(symget(''_METAPERSON''))); ';
put ' put '',"_METAPERSON": '' _METAPERSON; ';
put ' put '',"_PROGRAM" : '' _PROGRAM ; ';
put ' put ",""SYSCC"" : ""&syscc"" "; ';
put ' put ",""SYSERRORTEXT"" : ""&syserrortext"" "; ';
put ' put ",""SYSHOSTNAME"" : ""&syshostname"" "; ';
put ' put ",""SYSJOBID"" : ""&sysjobid"" "; ';
put ' put ",""SYSSITE"" : ""&syssite"" "; ';
put ' put ",""SYSWARNINGTEXT"" : ""&syswarningtext"" "; ';
put ' put '',"END_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''" ''; ';
put ' put "}" @; ';
put ' %if %str(&_debug) ge 131 %then %do; ';
put ' put ''>>weboutEND<<''; ';
put ' %end; ';
put ' run; ';
put '%end; ';
put ' ';
put '%mend; ';
put ' ';
put '%macro mf_getuser(type=META ';
put ')/*/STORE SOURCE*/; ';
put ' %local user metavar; ';
put ' %if &type=OS %then %let metavar=_secureusername; ';
put ' %else %let metavar=_metaperson; ';
put ' ';
put ' %if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %let user=&SYS_COMPUTE_SESSION_OWNER; ';
put ' %else %if %symexist(&metavar) %then %do; ';
put ' %if %length(&&&metavar)=0 %then %let user=&sysuserid; ';
put ' /* sometimes SAS will add @domain extension - remove for consistency */ ';
put ' %else %let user=%scan(&&&metavar,1,@); ';
put ' %end; ';
put ' %else %let user=&sysuserid; ';
put ' ';
put ' %quote(&user) ';
put ' ';
put '%mend; ';
/* WEBOUT END */
put '%macro webout(action,ds,dslabel=,fmt=);';
put ' %mm_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt)';
put '%mend;';
run;
/* add precode and code */
%local work tmpfile;
%let work=%sysfunc(pathname(work));
%let tmpfile=__mm_createwebservice.temp;
%local x fref freflist mod;
%let freflist= &adapter &precode &code ;
%do x=1 %to %sysfunc(countw(&freflist));
%if &x>1 %then %let mod=mod;
%let fref=%scan(&freflist,&x);
%put &sysmacroname: adding &fref;
data _null_;
file "&work/&tmpfile" lrecl=3000 &mod;
infile &fref;
input;
put _infile_;
run;
%end;
/* create the metadata folder if not already there */
%mm_createfolder(path=&path)
%if &syscc ge 4 %then %return;
%if %upcase(&replace)=YES %then %do;
%mm_deletestp(target=&path/&name)
%end;
/* create the web service */
%mm_createstp(stpname=&name
,filename=&tmpfile
,directory=&work
,tree=&path
,stpdesc=&desc
,mDebug=&mdebug
,server=&server
,stptype=2)
/* find the web app url */
%local url;
%let url=localhost/SASStoredProcess;
data _null_;
length url $128;
rc=METADATA_GETURI("Stored Process Web App",url);
if rc=0 then call symputx('url',url,'l');
run;
%put ;%put ;%put ;%put ;%put ;%put ;
%put &sysmacroname: STP &name successfully created in &path;
%put ;%put ;%put ;
%put Check it out here:;
%put ;%put ;%put ;
%put &url?_PROGRAM=&path/&name;
%put ;%put ;%put ;%put ;%put ;%put ;
%mend;

View File

@@ -0,0 +1,71 @@
/**
@file mm_deletedocument.sas
@brief Deletes a Document using path as reference
@details
Usage:
%mm_createdocument(tree=/User Folders/&sysuserid,name=MyNote)
%mm_deletedocument(target=/User Folders/&sysuserid/MyNote)
<h4> Dependencies </h4>
@param target= full path to the document being deleted
@version 9.4
@author Allan Bowe
**/
%macro mm_deletedocument(
target=
)/*/STORE SOURCE*/;
/**
* Check document exist
*/
%local type;
data _null_;
length type uri $256;
rc=metadata_pathobj("","&target",'Note',type,uri);
call symputx('type',type,'l');
call symputx('stpuri',uri,'l');
run;
%if &type ne Document %then %do;
%put WARNING: No Document found at &target;
%return;
%end;
filename __in temp lrecl=10000;
filename __out temp lrecl=10000;
data _null_ ;
file __in ;
put "<DeleteMetadata><Metadata><Document Id='&stpuri'/>";
put "</Metadata><NS>SAS</NS><Flags>268436480</Flags><Options/>";
put "</DeleteMetadata>";
run ;
proc metadata in=__in out=__out verbose;run;
/* list the result */
data _null_;infile __out; input; list; run;
filename __in clear;
filename __out clear;
/**
* Check deletion
*/
%local isgone;
data _null_;
length type uri $256;
call missing (of _all_);
rc=metadata_pathobj("","&target",'Note',type,uri);
call symputx('isgone',type,'l');
run;
%if &isgone = Document %then %do;
%put %str(ERR)OR: Document not deleted from &target;
%let syscc=4;
%return;
%end;
%mend;

70
meta/mm_deletestp.sas Normal file
View File

@@ -0,0 +1,70 @@
/**
@file mm_deletestp.sas
@brief Deletes a Stored Process using path as reference
@details Will only delete the metadata, not any physical files associated.
Usage:
%mm_deletestp(target=/some/meta/path/myStoredProcess)
<h4> Dependencies </h4>
@param target= full path to the STP being deleted
@version 9.4
@author Allan Bowe
**/
%macro mm_deletestp(
target=
)/*/STORE SOURCE*/;
/**
* Check STP does exist
*/
%local cmtype;
data _null_;
length type uri $256;
rc=metadata_pathobj("","&target",'StoredProcess',type,uri);
call symputx('cmtype',type,'l');
call symputx('stpuri',uri,'l');
run;
%if &cmtype ne ClassifierMap %then %do;
%put NOTE: No Stored Process found at &target;
%return;
%end;
filename __in temp lrecl=10000;
filename __out temp lrecl=10000;
data _null_ ;
file __in ;
put "<DeleteMetadata><Metadata><ClassifierMap Id='&stpuri'/>";
put "</Metadata><NS>SAS</NS><Flags>268436480</Flags><Options/>";
put "</DeleteMetadata>";
run ;
proc metadata in=__in out=__out verbose;run;
/* list the result */
data _null_;infile __out; input; list; run;
filename __in clear;
filename __out clear;
/**
* Check deletion
*/
%local isgone;
data _null_;
length type uri $256;
call missing (of _all_);
rc=metadata_pathobj("","&target",'Note',type,uri);
call symputx('isgone',type,'l');
run;
%if &isgone = ClassifierMap %then %do;
%put %str(ERR)OR: STP not deleted from &target;
%let syscc=4;
%return;
%end;
%mend;

116
meta/mm_getauthinfo.sas Normal file
View File

@@ -0,0 +1,116 @@
/**
@file mm_getauthinfo.sas
@brief extracts authentication info
@details usage:
%mm_getauthinfo(outds=auths)
@param outds= the ONE LEVEL work dataset to create
<h4> Dependencies </h4>
@li mm_getobjects.sas
@li mf_getuniquefileref.sas
@li mm_getdetails.sas
@version 9.4
@author Allan Bowe
**/
%macro mm_getauthinfo(outds=mm_getauthinfo
)/*/STORE SOURCE*/;
%if %length(&outds)>30 %then %do;
%put %str(ERR)OR: Temp tables are created with the &outds prefix, which therefore
needs to be 30 characters or less;
%return;
%end;
%if %index(&outds,'.')>0 %then %do;
%put %str(ERR)OR: Table &outds should be ONE LEVEL (no library);
%return;
%end;
%mm_getobjects(type=Login,outds=&outds.0)
%local fileref;
%let fileref=%mf_getuniquefileref();
data _null_;
file &fileref;
set &outds.0 end=last;
/* run macro */
str=cats('%mm_getdetails(uri=',id,",outattrs=&outds.d",_n_
,",outassocs=&outds.a",_n_,")");
put str;
/* transpose attributes */
str=cats("proc transpose data=&outds.d",_n_,"(drop=type) out=&outds.da"
,_n_,"(drop=_name_);var value;id name;run;");
put str;
/* add extra info to attributes */
str=cats("data &outds.da",_n_,";length login_id login_name $256; login_id="
,quote(trim(id)),";set &outds.da",_n_
,";login_name=trim(subpad(name,1,256));drop name;run;");
put str;
/* add extra info to associations */
str=cats("data &outds.a",_n_,";length login_id login_name $256; login_id="
,quote(trim(id)),";login_name=",quote(trim(name))
,";set &outds.a",_n_,";run;");
put str;
if last then do;
/* collate attributes */
str=cats("data &outds._logat; set &outds.da1-&outds.da",_n_,";run;");
put str;
/* collate associations */
str=cats("data &outds._logas; set &outds.a1-&outds.a",_n_,";run;");
put str;
/* tidy up */
str=cats("proc delete data=&outds.da1-&outds.da",_n_,";run;");
put str;
str=cats("proc delete data=&outds.d1-&outds.d",_n_,";run;");
put str;
str=cats("proc delete data=&outds.a1-&outds.a",_n_,";run;");
put str;
end;
run;
%inc &fileref;
/* get libraries */
proc sort data=&outds._logas(where=(assoc='Libraries')) out=&outds._temp;
by login_id;
data &outds._temp;
set &outds._temp;
by login_id;
length library_list $32767;
retain library_list;
if first.login_id then library_list=name;
else library_list=catx(' !! ',library_list,name);
proc sql;
/* get auth domain */
create table &outds._dom as
select login_id,name as domain
from &outds._logas
where assoc='Domain';
create unique index login_id on &outds._dom(login_id);
/* join it all together */
create table &outds._logins as
select a.*
,c.domain
,b.library_list
from &outds._logat (drop=ishidden lockedby usageversion publictype) a
left join &outds._temp b
on a.login_id=b.login_id
left join &outds._dom c
on a.login_id=c.login_id;
drop table &outds._temp;
drop table &outds._logat;
drop table &outds._logas;
data _null_;
infile &fileref;
if _n_=1 then putlog // "Now executing the following code:" //;
input; putlog _infile_;
run;
filename &fileref clear;
%mend;

53
meta/mm_getcols.sas Normal file
View File

@@ -0,0 +1,53 @@
/**
@file
@brief Creates a dataset with all metadata columns for a particular table
@details
usage:
%mm_getcols(tableuri=A5X8AHW1.B40001S5)
@param outds the dataset to create that contains the list of columns
@param uri the uri of the table for which to return columns
@returns outds dataset containing all columns, specifically:
- colname
- coluri
- coldesc
@version 9.2
@author Allan Bowe
**/
%macro mm_getcols(
tableuri=
,outds=work.mm_getcols
)/*/STORE SOURCE*/;
data &outds;
keep col: SAS:;
length assoc uri coluri colname coldesc SASColumnType SASFormat SASInformat
SASPrecision SASColumnLength $256;
call missing (of _all_);
uri=symget('tableuri');
n=1;
do while (metadata_getnasn(uri,'Columns',n,coluri)>0);
rc3=metadata_getattr(coluri,"Name",colname);
rc3=metadata_getattr(coluri,"Desc",coldesc);
rc4=metadata_getattr(coluri,"SASColumnType",SASColumnType);
rc5=metadata_getattr(coluri,"SASFormat",SASFormat);
rc6=metadata_getattr(coluri,"SASInformat",SASInformat);
rc7=metadata_getattr(coluri,"SASPrecision",SASPrecision);
rc8=metadata_getattr(coluri,"SASColumnLength",SASColumnLength);
output;
call missing(colname,coldesc,SASColumnType,SASFormat,SASInformat
,SASPrecision,SASColumnLength);
n+1;
end;
run;
proc sort;
by colname;
run;
%mend;

65
meta/mm_getdetails.sas Normal file
View File

@@ -0,0 +1,65 @@
/**
@file mm_getdetails.sas
@brief extracts metadata attributes and associations for a particular uri
@param uri the metadata object for which to return attributes / associations
@param outattrs= the dataset to create that contains the list of attributes
@param outassocs= the dataset to contain the list of associations
@version 9.2
@author Allan Bowe
**/
%macro mm_getdetails(uri
,outattrs=work.attributes
,outassocs=work.associations
)/*/STORE SOURCE*/;
data &outassocs;
keep assoc assocuri name;
length assoc assocuri name $256;
call missing(of _all_);
rc1=1;n1=1;
do while(rc1>0);
/* Walk through all possible associations of this object. */
rc1=metadata_getnasl("&uri",n1,assoc);
rc2=1;n2=1;
do while(rc2>0);
/* Walk through all the associations on this machine object. */
rc2=metadata_getnasn("&uri",trim(assoc),n2,assocuri);
if (rc2>0) then do;
rc3=metadata_getattr(assocuri,"Name",name);
output;
end;
call missing(name,assocuri);
n2+1;
end;
n1+1;
end;
run;
proc sort;
by assoc name;
run;
data &outattrs;
keep type name value;
length type $4 name $256 value $32767;
rc1=1;n1=1;type='Prop';
do while(rc1>0);
rc1=metadata_getnprp("&uri",n1,name,value);
if rc1>0 then output;
n1+1;
end;
rc1=1;n1=1;type='Attr';
do while(rc1>0);
rc1=metadata_getnatr("&uri",n1,name,value);
if rc1>0 then output;
n1+1;
end;
run;
proc sort;
by type name;
run;
%mend;

55
meta/mm_getdirectories.sas Executable file
View File

@@ -0,0 +1,55 @@
/**
@file
@brief Returns a dataset with the meta directory object for a physical path
@details Provide a file path to get matching directory objects, or leave
blank to return all directories. The Directory object is used to reference
a physical filepath (eg when registering a .sas program in a Stored process)
@param path= the physical path for which to return a meta Directory object
@param outds= the dataset to create that contains the list of directories
@param mDebug= set to 1 to show debug messages in the log
@returns outds dataset containing the following columns:
- directoryuri
- groupname
- groupdesc
@version 9.2
@author Allan Bowe
**/
%macro mm_getDirectories(
path=
,outds=work.mm_getDirectories
,mDebug=0
)/*/STORE SOURCE*/;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing mm_getDirectories.sas;
%&mD.put _local_;
data &outds (keep=directoryuri name directoryname directorydesc );
length directoryuri name directoryname directorydesc $256;
call missing(of _all_);
__i+1;
%if %length(&path)=0 %then %do;
do while
(metadata_getnobj("omsobj:Directory?@Id contains '.'",__i,directoryuri)>0);
%end; %else %do;
do while
(metadata_getnobj("omsobj:Directory?@DirectoryName='&path'",__i,directoryuri)>0);
%end;
__rc1=metadata_getattr(directoryuri, "Name", name);
__rc2=metadata_getattr(directoryuri, "DirectoryName", directoryname);
__rc3=metadata_getattr(directoryuri, "Desc", directorydesc);
&mD.putlog (_all_) (=);
drop __:;
__i+1;
if sum(of __rc1-__rc3)=0 then output;
end;
run;
%mend;

145
meta/mm_getdocument.sas Normal file
View File

@@ -0,0 +1,145 @@
/**
@file
@brief Writes the TextStore of a Document Object to an external file
@details If the document exists, and has a textstore object, the contents
of that textstore are written to an external file.
usage:
%mm_getdocument(tree=/some/meta/path
,name=someDocument
,outref=/some/unquoted/filename.ext
)
<h4> Dependencies </h4>
@li mp_abort.sas
@param tree= The metadata path of the document
@param name= Document object name.
@param outref= full and unquoted path to the desired text file. This will be
overwritten if it already exists.
@author Allan Bowe
**/
%macro mm_getdocument(
tree=/User Folders/sasdemo
,name=myNote
,outref=%sysfunc(pathname(work))/mm_getdocument.txt
,mDebug=1
);
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing &sysmacroname..sas;
%&mD.put _local_;
/**
* check tree exists
*/
data _null_;
length type uri $256;
rc=metadata_pathobj("","&tree","Folder",type,uri);
call symputx('type',type,'l');
call symputx('treeuri',uri,'l');
run;
%mp_abort(
iftrue= (&type ne Tree)
,mac=mm_getdocument.sas
,msg=Tree &tree does not exist!
)
/**
* Check object exists
*/
data _null_;
length type docuri tsuri tsid $256 ;
rc1=metadata_pathobj("","&tree/&name","Note",type,docuri);
rc2=metadata_getnasn(docuri,"Notes",1,tsuri);
rc3=metadata_getattr(tsuri,"Id",tsid);
call symputx('type',type,'l');
call symputx("tsid",tsid,'l');
putlog (_all_)(=);
run;
%mp_abort(
iftrue= (&type ne Document)
,mac=mm_getdocument.sas
,msg=Document &name could not be found in &tree!
)
/**
* Now we can extract the textstore
*/
filename __getdoc temp lrecl=10000000;
proc metadata
in="<GetMetadata><Reposid>$METAREPOSITORY</Reposid>
<Metadata><TextStore Id='&tsid'/></Metadata>
<Ns>SAS</Ns><Flags>1</Flags><Options/></GetMetadata>"
out=__getdoc ;
run;
/* find the beginning of the text */
data _null_;
infile __getdoc lrecl=10000;
input;
start=index(_infile_,'StoredText="');
if start then do;
call symputx("start",start+11);
put start= "type=&type";
putlog '"' _infile_ '"';
end;
stop;
/* read the content, byte by byte, resolving escaped chars */
filename __outdoc "&outref" lrecl=100000;
data _null_;
length filein 8 fileid 8;
filein = fopen("__getdoc","I",1,"B");
fileid = fopen("__outdoc","O",1,"B");
rec = "20"x;
length entity $6;
do while(fread(filein)=0);
x+1;
if x>&start then do;
rc = fget(filein,rec,1);
if rec='"' then leave;
else if rec="&" then do;
entity=rec;
do until (rec=";");
if fread(filein) ne 0 then goto getout;
rc = fget(filein,rec,1);
entity=cats(entity,rec);
end;
select (entity);
when ('&amp;' ) rec='&' ;
when ('&lt;' ) rec='<' ;
when ('&gt;' ) rec='>' ;
when ('&apos;') rec="'" ;
when ('&quot;') rec='"' ;
when ('&#x0a;') rec='0A'x;
when ('&#x0d;') rec='0D'x;
when ('&#36;' ) rec='$' ;
otherwise putlog "WARNING: missing value for " entity=;
end;
rc =fput(fileid, substr(rec,1,1));
rc =fwrite(fileid);
end;
else do;
rc =fput(fileid,rec);
rc =fwrite(fileid);
end;
end;
end;
getout:
rc=fclose(filein);
rc=fclose(fileid);
run;
filename __getdoc clear;
filename __outdoc clear;
%mend;

88
meta/mm_getfoldertree.sas Normal file
View File

@@ -0,0 +1,88 @@
/**
@file mm_getfoldertree.sas
@brief Returns all folders / subfolder content for a particular root
@details Shows all members and SubTrees recursively for a particular root.
Note - for big sites, this returns a lot of data! So you may wish to reduce
the logging to speed up the process (see example below)
Usage:
options ps=max nonotes nosource;
%mm_getfoldertree(root=/My/Meta/Path, outds=iwantthisdataset)
options notes source;
@param root= the parent folder under which to return all contents
@param outds= the dataset to create that contains the list of directories
@param mDebug= set to 1 to show debug messages in the log
<h4> Dependencies </h4>
@version 9.4
@author Allan Bowe
**/
%macro mm_getfoldertree(
root=
,outds=work.mm_getfoldertree
,mDebug=0
,depth=50 /* how many nested folders to query */
,level=1 /* system var - to track current level depth */
,append=NO /* system var - when YES means appending within nested loop */
)/*/STORE SOURCE*/;
%if &level>&depth %then %return;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing &sysmacroname;
%&mD.put _local_;
%if &append=NO %then %do;
/* ensure table doesn't exist already */
data &outds; run;
proc sql; drop table &outds;
%end;
/* get folder contents */
data &outds.TMP/view=&outds.TMP;
length metauri pathuri $64 name $256 path $1024
assoctype publictype MetadataUpdated MetadataCreated $32;
keep metauri assoctype name publictype MetadataUpdated MetadataCreated path;
call missing(of _all_);
path="&root";
rc=metadata_pathobj("",path,"Folder",publictype,pathuri);
if publictype ne 'Tree' then do;
putlog "%str(WAR)NING: Tree " path 'does not exist!' publictype=;
stop;
end;
__n1=1;
do while(metadata_getnasl(pathuri,__n1,assoctype)>0);
__n1+1;
/* Walk through all possible associations of this object. */
__n2=1;
if assoctype in ('Members','SubTrees') then
do while(metadata_getnasn(pathuri,assoctype,__n2,metauri)>0);
__n2+1;
call missing(name,publictype,MetadataUpdated,MetadataCreated);
__rc1=metadata_getattr(metauri,"Name", name);
__rc2=metadata_getattr(metauri,"MetadataUpdated", MetadataUpdated);
__rc3=metadata_getattr(metauri,"MetadataCreated", MetadataCreated);
__rc4=metadata_getattr(metauri,"PublicType", PublicType);
output;
end;
n1+1;
end;
drop __:;
run;
proc append base=&outds data=&outds.TMP;
run;
data _null_;
set &outds.TMP(where=(assoctype='SubTrees'));
call execute('%mm_getfoldertree(root='
!!cats(path,"/",name)!!",outds=&outds,mDebug=&mdebug,depth=&depth"
!!",level=%eval(&level+1),append=YES)");
run;
%mend;

Some files were not shown because too many files have changed in this diff Show More