mirror of
https://github.com/yabwon/SAS_PACKAGES.git
synced 2026-01-06 06:40:06 +00:00
SAS Packages Framework, version 20251231
SAS Packages Framework, version 20251231
Changes:
- New macro: %bundlePackages().
- New macro: %unbundlePackages().
- Bug fix in %verifyPackage() macro.
- Minor updates in %generatePackage(), %listPackages(), and %relocatePackage() macros.
- Documentation updated.
This commit is contained in:
502
SPF/Macros/bundlePackages.sas
Normal file
502
SPF/Macros/bundlePackages.sas
Normal file
@@ -0,0 +1,502 @@
|
||||
/*+bundlePackages+*/
|
||||
%macro bundlePackages(
|
||||
bundleName
|
||||
,path=
|
||||
,pathRef=
|
||||
,packagesList=
|
||||
,packagesPath=
|
||||
,packagesRef=packages
|
||||
,ods= /* data set for report file */
|
||||
)/
|
||||
des='Macro to create a bundle of SAS packages, version 20251231. Run %bundlePackages(HELP) for help info.'
|
||||
secure minoperator
|
||||
;
|
||||
|
||||
%if /*(%superq(bundleName) = ) OR*/ (%qupcase(&bundleName.) = HELP) %then
|
||||
%do;
|
||||
%local options_tmp ;
|
||||
%let options_tmp = ls=%sysfunc(getoption(ls)) ps=%sysfunc(getoption(ps))
|
||||
%sysfunc(getoption(notes)) %sysfunc(getoption(source))
|
||||
msglevel=%sysfunc(getoption(msglevel))
|
||||
;
|
||||
options NOnotes NOsource ls=MAX ps=MAX msglevel=N;
|
||||
%put ;
|
||||
%put #################################################################################;
|
||||
%put ### This is short help information for the `bundlePackages` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to *create bundles* of SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
%put # data, etc.) wrapped up together and included by #;
|
||||
%put # a single `load.sas` file (also embedded inside the zip). #;
|
||||
%put # #;
|
||||
%put # The `%nrstr(%%bundlePackages())` macro allows to bundle a bunch of SAS packages #;
|
||||
%put # into a single file (a SAS packages bundle), just like a snapshot. #;
|
||||
%put # #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put #### Parameters: #;
|
||||
%put # #;
|
||||
%put # 1. `bundleName` *Required.* Name of a bundle, e.g. myBundle, #;
|
||||
%put # if the value is empty a default name is generated: #;
|
||||
%put # `saspackagesbundle_createdYYYYMMDDtHHMMSS`, an #;
|
||||
%put # extension `.bundle.zip` is automatically added. #;
|
||||
%put # For value `HELP` this help information is displayed. #;
|
||||
%put # #;
|
||||
%put # - `path=` *Required.* Location of the bundle. Must be a valid #;
|
||||
%put # directory. Takes precedence over `pathRef` parameter. #;
|
||||
%put # Either `path=` or `pathRef=` must be non-empty. #;
|
||||
%put # #;
|
||||
%put # - `pathRef=` *Optional.* Fileref to location of the bundle. #;
|
||||
%put # Either `path=` or `pathRef=` must be non-empty. #;
|
||||
%put # #;
|
||||
%put # - `packagesList=` *Optional.* A space-separated list of packages #;
|
||||
%put # to bundle. If the value is empty all available #;
|
||||
%put # packages are used. #;
|
||||
%put # #;
|
||||
%put # - `packagesPath=` *Optional.* Location of packages for the bundle. #;
|
||||
%put # Takes precedence over `packagesRef` parameter. #;
|
||||
%put # When non-empty, must be a valid directory. #;
|
||||
%put # #;
|
||||
%put # - `packagesRef=` *Optional.* Fileref to location of packages for the #;
|
||||
%put # bundle. Default value is `packages`. #;
|
||||
%put # #;
|
||||
%put # - `ods=` *Optional.* Name of SAS data set for the report. #;
|
||||
%put # #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Visit: `https://github.com/yabwon/SAS_PACKAGES/tree/main/SPF/Documentation` #;
|
||||
%put # to learn more. #;
|
||||
%put # Tutorials available at: `https://github.com/yabwon/HoW-SASPackages` #;
|
||||
%put # #;
|
||||
%put ### Example 1 ###################################################################;
|
||||
%put # #;
|
||||
%put # Enabling the SAS Package Framework #;
|
||||
%put # from the local directory and create a bundle of #;
|
||||
%put # selected packages in user home directory. #;
|
||||
%put # #;
|
||||
%put # Assume that the `SPFinit.sas` file #;
|
||||
%put # is located in the "/sas/PACKAGES/" folder. #;
|
||||
%put # #;
|
||||
%put # Run the following code in your SAS session: #;
|
||||
%put ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~sas;
|
||||
%put %nrstr( filename packages "/sas/PACKAGES/"; %%* setup a directory for packages;);
|
||||
%put %nrstr( %%include packages(SPFinit.sas); %%* enable the framework; );
|
||||
%put ;
|
||||
%put %nrstr( %%bundlePackages%(myLittleBundle );
|
||||
%put %nrstr( ,path=/home/user/bundles );
|
||||
%put %nrstr( ,packagesList=basePlus SQLinDS macroarray );
|
||||
%put %nrstr( ,packagesRef=PACKAGES%) );
|
||||
%put ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~;
|
||||
%put # #;
|
||||
%put #################################################################################;
|
||||
%put ;
|
||||
options &options_tmp.;
|
||||
%GOTO ENDofbundlePackages;
|
||||
%end;
|
||||
/* local variables for options */
|
||||
%local ls_tmp ps_tmp notes_tmp source_tmp stimer_tmp fullstimer_tmp msglevel_tmp mautocomploc_tmp;
|
||||
%let ls_tmp = %sysfunc(getoption(ls));
|
||||
%let ps_tmp = %sysfunc(getoption(ps));
|
||||
%let notes_tmp = %sysfunc(getoption(notes));
|
||||
%let source_tmp = %sysfunc(getoption(source));
|
||||
%let stimer_tmp = %sysfunc(getoption(stimer));
|
||||
%let fullstimer_tmp = %sysfunc(getoption(fullstimer));
|
||||
%let msglevel_tmp = %sysfunc(getoption(msglevel));
|
||||
%let mautocomploc_tmp = %sysfunc(getoption(mautocomploc));
|
||||
|
||||
options NOnotes NOsource ls=128 ps=MAX NOfullstimer NOstimer msglevel=N NOmautocomploc;
|
||||
/*===================================================================================================*/
|
||||
|
||||
%local HASHING_FILE_exist;
|
||||
%let HASHING_FILE_exist = 0;
|
||||
|
||||
%if %sysfunc(exist(sashelp.vfunc, VIEW)) %then
|
||||
%do;
|
||||
data _null_;
|
||||
set sashelp.vfunc(keep=fncname);
|
||||
where fncname = "HASHING_FILE";
|
||||
call symputX('HASHING_FILE_exist', 1, "L");
|
||||
stop;
|
||||
run;
|
||||
%end;
|
||||
|
||||
%local reportFile datetime;
|
||||
%let datetime = %sysfunc(datetime());
|
||||
%let reportFile = WORK.tmpbundlefile%sysfunc(int(&datetime.), b8601dt15.)_;
|
||||
|
||||
data _null_ %if %superq(ods) NE %then %do; &ods. %end;
|
||||
%else %do; &reportFile.1 %end;
|
||||
;
|
||||
datetime=symgetn('datetime');
|
||||
|
||||
length packagesList $ 32767 bundleName $ 128;
|
||||
packagesList = lowcase(compress(symget('packagesList'),"_ ","KAD")); /* keep only "proper" packages names */
|
||||
bundleName = compress(symget('bundleName'),"_","KAD"); /* bundle name is letters, digits, and underscore, up to 128 symbols */
|
||||
|
||||
if bundleName NE symget('bundleName') then /* warn about illegal characters */
|
||||
do;
|
||||
put "WARNING: Bundle name has illegal characters, name will be modified.";
|
||||
end;
|
||||
if " "=bundleName then bundleName=cats("SASPackagesBundle_created", put(datetime,b8601dt.));
|
||||
|
||||
bundleName=lowcase(bundleName);
|
||||
put / "INFO: Bundle name is: " bundleName / ;
|
||||
|
||||
length packagesPath $ 32767 packagesRef $ 8;
|
||||
packagesPath = dequote(symget('packagesPath'));
|
||||
packagesRef = upcase(strip(symget('packagesRef')));
|
||||
|
||||
/* organize source path (location of packages) */
|
||||
if " "=packagesPath then
|
||||
do;
|
||||
if 0 then set SASHELP.VEXTFL(keep=level xpath xengine fileref exists);
|
||||
DECLARE HASH sH(dataset:'SASHELP.VEXTFL(where=(fileref=' !! quote(packagesRef) !! '))', ordered: "A");
|
||||
sH.DefineKey("level");
|
||||
sH.DefineData("xpath","xengine","exists");
|
||||
sH.DefineDone();
|
||||
DECLARE HITER sI("sH");
|
||||
|
||||
if sH.NUM_ITEMS=0 then
|
||||
do;
|
||||
put "ERROR: Fileref in packagesRef= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
packagesPath=" ";
|
||||
|
||||
if 1=sH.NUM_ITEMS then
|
||||
do;
|
||||
rc = sH.FIND(key:0);
|
||||
if xengine = "DISK" AND exists='yes' then
|
||||
packagesPath=quote(strip(xpath)); /* add quotes to the packagesPath */
|
||||
else
|
||||
put "WARNING: Path: " xpath "in packagesRef= is invalid! Path ignored!";
|
||||
end;
|
||||
else
|
||||
do i = 1 to sum(sH.NUM_ITEMS,0);
|
||||
rc = sH.FIND(key:i);
|
||||
if exists='no'
|
||||
then put "WARNING: Path: " xpath "in packagesRef= does NOT exist! Path ignored!";
|
||||
else if xengine NE "DISK"
|
||||
then put "WARNING: Engine in packagesRef= is not DISK! Path ignored!";
|
||||
else packagesPath = catx(" ", packagesPath, quote(strip(xpath))); /* add quotes to the packagesPath */
|
||||
end;
|
||||
|
||||
if " "=packagesPath then
|
||||
do;
|
||||
put "ERROR: Invalid directory in packagesRef=. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
if 1 < sH.NUM_ITEMS then packagesPath = cats("(", packagesPath, ')'); /* add brackets for multi-level path */
|
||||
|
||||
end;
|
||||
else
|
||||
do;
|
||||
rcPckPath = fileexist(strip(packagesPath));
|
||||
if 0=rcPckPath then
|
||||
do;
|
||||
put "ERROR: Path in packagesPath= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
else packagesPath=quote(strip(packagesPath)); /* add quotes to the packagesPath */
|
||||
end;
|
||||
|
||||
length path $ 32767 pathRef $ 8;
|
||||
path = dequote(symget('path'));
|
||||
pathRef = upcase(strip(symget('pathRef')));
|
||||
|
||||
if " "=path and " "=pathRef then
|
||||
do;
|
||||
put "ERROR: Path= and pathRef= are empty! Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
/* verify target path (location of bundle) */
|
||||
if " "=path then
|
||||
do;
|
||||
DECLARE HASH tH(dataset:'SASHELP.VEXTFL(where=(fileref=' !! quote(pathRef) !! '))', ordered: "A");
|
||||
tH.DefineKey("level");
|
||||
tH.DefineData("xpath","xengine","exists");
|
||||
tH.DefineDone();
|
||||
DECLARE HITER tI("tH");
|
||||
|
||||
if tH.NUM_ITEMS=0 then
|
||||
do;
|
||||
put "ERROR: Fileref in pathRef= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
rc = tI.first();
|
||||
if exists='no' then
|
||||
do;
|
||||
put "ERROR: Fileref in pathRef= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
path = strip(xpath);
|
||||
end;
|
||||
else
|
||||
do;
|
||||
rcPath = fileexist(strip(path));
|
||||
if 0=rcPath then
|
||||
do;
|
||||
put "ERROR: Path in Path= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
end;
|
||||
|
||||
/* get the list of packages to bundle, don't worry if list is empty */
|
||||
length pckNm pckVer pckDtm $ 24;
|
||||
DECLARE HASH P(ordered:"A");
|
||||
P.defineKey("pckNm");
|
||||
P.defineDone();
|
||||
DECLARE HASH Q(ordered:"A");
|
||||
Q.defineKey("pckNm");
|
||||
Q.defineData("pckNm",'pckVer','pckDtm');
|
||||
Q.defineDone();
|
||||
DECLARE HITER IQ("Q");
|
||||
if " " NE packagesList then
|
||||
do k=1 to countw(packagesList, " ");
|
||||
pckNm = strip(scan(packagesList,k, " "));
|
||||
rc = P.replace();
|
||||
end;
|
||||
packagesList = " ";
|
||||
|
||||
/* select all packages from source and intersect them with the list in packagesList ... */
|
||||
put "INFO: List of packages available for bundle: ";
|
||||
do k = 1 to kcountw(packagesPath, "()", "QS");
|
||||
length base $ 1024;
|
||||
base = dequote(kscanx(packagesPath, k, "()", "QS"));
|
||||
|
||||
length folder $ 64 file $ 1024 folderRef fileRef packageMetadata $ 8;
|
||||
|
||||
rc=filename(folderRef, base);
|
||||
folderid=dopen(folderRef);
|
||||
|
||||
do i=1 to dnum(folderId);
|
||||
folder = dread(folderId, i);
|
||||
|
||||
rc = filename(fileRef, catx("/", base, folder));
|
||||
fileId = dopen(fileRef);
|
||||
|
||||
EOF = 0;
|
||||
if fileId = 0 and lowcase(kscanx(folder, -1, ".")) = 'zip' then
|
||||
do;
|
||||
file = catx('/',base, folder);
|
||||
|
||||
rc1 = filename(packageMetadata, strip(file), 'zip', 'member="packagemetadata.sas"');
|
||||
rcE = fexist(packageMetadata);
|
||||
rc2 = filename(packageMetadata);
|
||||
|
||||
if rcE then /* if the packagemetadata.sas exists in the zip then check if package is on the list */
|
||||
do;
|
||||
pckNm = strip(scan(folder,1,"."));
|
||||
|
||||
if (0 = P.NUM_ITEMS) OR (0=P.find()) then
|
||||
do;
|
||||
pckVer='_._._';
|
||||
pckDtm="____-__-__T__:__:__";
|
||||
/*--------------------------------------------------*/
|
||||
infile _DUMMY_ ZIP FILEVAR=file member="packagemetadata.sas" end=EOF;
|
||||
do until(EOF);
|
||||
input;
|
||||
/*putlog ">>" _infile_;*/
|
||||
select( lowcase(kscanx(_INFILE_,2,"(,)")) );
|
||||
when ('"packageversion"' ) pckVer=dequote(strip(kscanx(_INFILE_,3,"(,)")));
|
||||
when ('"packagegenerated"') pckDtm=dequote(strip(kscanx(_INFILE_,3,"(,)")));
|
||||
otherwise;
|
||||
end;
|
||||
end;
|
||||
/*--------------------------------------------------*/
|
||||
pckVer=coalescec(pckVer,'_._._');
|
||||
pckDtm=coalescec(pckDtm,"____-__-__T__:__:__");
|
||||
|
||||
if (pckVer='_._._' OR pckDtm="____-__-__T__:__:__") then
|
||||
do;
|
||||
put "WARNING: Incomplete metadata for package: " pckNm +(-1) "!";
|
||||
rc = -1; /* ignore incomplete packages */
|
||||
end;
|
||||
else rc = Q.ADD();
|
||||
|
||||
if 0=rc then put base pckNm=;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
rc = dclose(fileId);
|
||||
rc = filename(fileRef);
|
||||
end;
|
||||
|
||||
rc = dclose(folderid);
|
||||
rc = filename(folderRef);
|
||||
end;
|
||||
|
||||
if 0=Q.NUM_ITEMS then /* ... if empty then exit */
|
||||
do;
|
||||
put "WARNING: No packages to bundle. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
else
|
||||
do while(iQ.next()=0);
|
||||
packagesList = catx(" ", packagesList, pckNm);
|
||||
end;
|
||||
|
||||
if 0 < P.NUM_ITEMS NE Q.NUM_ITEMS then
|
||||
do;
|
||||
put "WARNING: Not all packages listed for bundling were found.";
|
||||
end;
|
||||
|
||||
rc = Q.output(dataset:"&reportFile.3");
|
||||
|
||||
/* code executed for bundling */
|
||||
length code1 code2 $ 32767;
|
||||
code1=
|
||||
'options ps=min nofullstimer nostimer msglevel=N; filename PACKAGES ' !! strip(packagesPath) !! ';' !!
|
||||
'%relocatePackage(' !! strip(packagesList) !! ',target=' !! catx("/", path, bundleName) !!
|
||||
'.bundle.zip, tDevice=ZIP,psMAX=MIN,ods=&reportFile.2(keep=package sFilename s_HASHING));';
|
||||
code2=
|
||||
'options noNotes;' !!
|
||||
'filename _ ZIP ' !! quote(cats(path, "/", bundleName, ".bundle.zip")) !! ';' !!
|
||||
'data _null_;set &reportFile.2;file _(verification.sas);' !!
|
||||
'if 1=_N_ then put "/*" 64*"*" / "bundle created: ' !! put(datetime,e8601dt.) !! '" / 64*"*" "*/" /;' !!
|
||||
'put ''%verifyPackage('' package +(-1) ",hash=F*" s_HASHING +(-1)")";run;' !!
|
||||
'data &reportFile.4;merge &reportFile.2 &reportFile.3(rename=(pckNm=package));' !!
|
||||
'by package;file _(bundlecontent.sas) dsd;hash="F*"!!s_HASHING; put package pckVer pckDtm hash;run;' !!
|
||||
'title1 "Bundle: ' !! strip(bundleName) !! '";' !!
|
||||
'title2 "Summary of bundling process";' !!
|
||||
'proc print data=&reportFile.4 label;' !!
|
||||
'var package pckVer pckDtm hash sFilename;' !!
|
||||
'label package="Package name" pckVer="Version" pckDtm="Generation timestamp" sFilename="Source file location" hash="SHA256 for the Package";' !!
|
||||
'proc delete data=&reportFile.2 &reportFile.3 &reportFile.4;run;title;';
|
||||
|
||||
/*put code=;*/
|
||||
|
||||
put "INFO: The " bundleName "bundle creation in progress...";
|
||||
|
||||
rc = doSubL(code1);
|
||||
rc = doSubL(code2);
|
||||
|
||||
put "INFO: The " bundleName "bundle creation ended.";
|
||||
|
||||
%if &HASHING_FILE_exist. = 1 %then
|
||||
%do;
|
||||
rc = filename(fileRef, cats(path, "/", bundleName, ".bundle.zip"), "DISK", "lrecl=1 recfm=n");
|
||||
rctxt=sysmsg();
|
||||
if rc=0 then BundleSHA256 = "F*" !! HASHING_FILE("SHA256", pathname(fileRef,'F'), 0);
|
||||
else put rctxt=;
|
||||
put "INFO: SHA256 for the bundle is: " / @7 BundleSHA256;
|
||||
rc = filename(fileRef);
|
||||
%end;
|
||||
|
||||
|
||||
keep path bundleName BundleSHA256 datetime;
|
||||
label path = "Bundle location"
|
||||
bundleName = "Bundle name"
|
||||
BundleSHA256 = "SHA256 for the Bundle"
|
||||
datetime = "Bundle generation timestamp"
|
||||
;
|
||||
format datetime e8601dt.;
|
||||
output
|
||||
%if %superq(ods) NE %then %do; %scan(&ods.,1,()) %end;
|
||||
%else %do; &reportFile.1 %end;
|
||||
;
|
||||
put " ";
|
||||
rc=sleep(1,1);
|
||||
stop;
|
||||
run;
|
||||
|
||||
title2 "Summary of the bundle file";;
|
||||
proc print
|
||||
data= %if %superq(ods) NE %then %do; %scan(&ods.,1,()) %end;
|
||||
%else %do; &reportFile.1 %end;
|
||||
noObs label;
|
||||
var bundleName datetime BundleSHA256 path;
|
||||
run;
|
||||
%if %superq(ods) NE %then %do; %put INFO: Report file: %scan(&ods.,1,()); %end;
|
||||
%else %do; proc delete data=&reportFile.1; run; %end;
|
||||
|
||||
|
||||
/*===================================================================================================*/
|
||||
/* restore optionos */
|
||||
options ls = &ls_tmp. ps = &ps_tmp.
|
||||
¬es_tmp. &source_tmp.
|
||||
&stimer_tmp. &fullstimer_tmp.
|
||||
msglevel=&msglevel_tmp. &mautocomploc_tmp.;
|
||||
|
||||
%ENDofbundlePackages:
|
||||
%mend bundlePackages;
|
||||
|
||||
/*
|
||||
filename packages ("C:\SAS_WORK\SAS_PACKAGES" "C:\SAS_PACKAGES_DEV" "R:\");
|
||||
|
||||
options mprint ls=64 ps=max;
|
||||
%bundlePackages(
|
||||
bundleNameTest123
|
||||
,path=R:/
|
||||
,ods=work.summaryofthebundlefile
|
||||
)
|
||||
|
||||
%bundlePackages(
|
||||
bundleNameTest124
|
||||
,path=R:/
|
||||
,packagesList=basePlus SQLinDS macroarray ABCDEF functionsimissinbase
|
||||
,ods=work.summaryofthebundlefile1
|
||||
)
|
||||
|
||||
data _null_;
|
||||
set work.summaryofthebundlefile1;
|
||||
call symputX("hashCheck",BundleSHA256);
|
||||
run;
|
||||
%verifyPackage(
|
||||
bundlenametest124.bundle
|
||||
,hash=&hashCheck.
|
||||
)
|
||||
|
||||
%bundlePackages(
|
||||
bundleNameTest125
|
||||
,path=R:/dontexist
|
||||
,packagesList=basePlus SQLinDS macroarray ABCDEF
|
||||
)
|
||||
|
||||
options mprint ls=64 ps=max;
|
||||
%bundlePackages(
|
||||
bundleNameTest125
|
||||
,pathRef=p
|
||||
,packagesList=basePlus SQLinDS macroarray ABCDEF
|
||||
)
|
||||
|
||||
bundleNameTest126
|
||||
,path=R:\
|
||||
,packagesList=basePlus SQLinDS macroarray ABCDEF
|
||||
,packagesPath=R:/dontexist
|
||||
,packagesRef=packages
|
||||
)
|
||||
|
||||
filename p2 "R:/dontexist";
|
||||
%bundlePackages(
|
||||
bundleNameTest127
|
||||
,path=R:\
|
||||
,packagesList=basePlus SQLinDS macroarray ABCDEF
|
||||
,packagesRef=p2
|
||||
)
|
||||
|
||||
%bundlePackages(
|
||||
bundleNameTest128
|
||||
,path=R:\
|
||||
,packagesList=basePlus SQLinDS macroarray ABCDEF
|
||||
,packagesPath=R:/nopackages
|
||||
)
|
||||
|
||||
%bundlePackages(
|
||||
,path=R:/
|
||||
,ods=work.summaryofthebundlefile
|
||||
)
|
||||
|
||||
%bundlePackages(HELP)
|
||||
|
||||
%bundlePackages()
|
||||
*/
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
when empty the "packages" value is used */
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to list directories pointed by "packages" fileref, version 20251228. Run %extendPackagesFileref(HELP) for help info.'
|
||||
des = 'Macro to list directories pointed by "packages" fileref, version 20251231. Run %extendPackagesFileref(HELP) for help info.'
|
||||
;
|
||||
|
||||
%if %QUPCASE(&packages.) = HELP %then
|
||||
@@ -22,7 +22,7 @@ des = 'Macro to list directories pointed by "packages" fileref, version 20251228
|
||||
%put ### This is short help information for the `extendPackagesFileref` macro #;
|
||||
%put #-----------------------------------------------------------------------------------------#;;
|
||||
%put # #;
|
||||
%put # Macro to list directories pointed by 'packages' fileref, version `20251228` #;
|
||||
%put # Macro to list directories pointed by 'packages' fileref, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Macro to generate SAS packages.
|
||||
|
||||
Version 20251228
|
||||
Version 20251231
|
||||
|
||||
A SAS package is a zip file containing a group
|
||||
of SAS codes (macros, functions, data steps generating
|
||||
@@ -53,7 +53,7 @@
|
||||
when empty takes buildLocation */
|
||||
)/ secure minoperator
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to generate SAS packages, version 20251228. Run %generatePackage() for help info.'
|
||||
des = 'Macro to generate SAS packages, version 20251231. Run %generatePackage() for help info.'
|
||||
;
|
||||
%if (%superq(filesLocation) = ) OR (%qupcase(&filesLocation.) = HELP) %then
|
||||
%do;
|
||||
@@ -68,7 +68,7 @@ des = 'Macro to generate SAS packages, version 20251228. Run %generatePackage()
|
||||
%put ### This is short help information for the `generatePackage` macro #;
|
||||
%put #------------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to generate SAS packages, version `20251228` #;
|
||||
%put # Macro to generate SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
@@ -166,7 +166,7 @@ des = 'Macro to generate SAS packages, version 20251228. Run %generatePackage()
|
||||
%let filesWithCodes = WORK._%sysfunc(datetime(), hex16.)_;
|
||||
%let _DESCR_ = _%sysfunc(datetime(), hex6.)d;
|
||||
%let _LIC_ = _%sysfunc(datetime(), hex6.)l;
|
||||
%let _DUMMY_ = _%sysfunc(datetime(), hex6.)_;
|
||||
%let _DUMMY_ = d%sysfunc(datetime(), hex6.)d;
|
||||
|
||||
/* Verify source existence */
|
||||
%if 0=%sysfunc(FILEEXIST(%superq(filesLocation))) %then
|
||||
@@ -195,6 +195,16 @@ des = 'Macro to generate SAS packages, version 20251228. Run %generatePackage()
|
||||
%end;
|
||||
%put NOTE: Build location is: %superq(buildLocation).;
|
||||
|
||||
data _null_;
|
||||
length path $ 4096;
|
||||
do x='filesLocation','buildLocation';
|
||||
path = symget(x);
|
||||
str = kcompress(path,'({[<_ !@#$%^&*-=+/\?"'';:|~`>]})','pdfs');
|
||||
if str NE " " then put "NOTE: There are non-ASCII characters in the " x "path: " str
|
||||
/ "NOTE- If you are working with SAS9.4M7 or earlier it may cause problems.";
|
||||
end;
|
||||
run;
|
||||
|
||||
/* collect package metadata from the description.sas file */
|
||||
filename &_DESCR_. "&filesLocation./description.sas" lrecl = 1024;
|
||||
/* file contains licence */
|
||||
@@ -940,7 +950,7 @@ title6 "MD5 hashed fileref of package lowcase name: &_PackageFileref_.";
|
||||
title&_titleNumber_. "Package ZIP file location is: &buildLocation.";
|
||||
%end;
|
||||
|
||||
footnote1 "SAS Packages Framework, version 20251228";
|
||||
footnote1 "SAS Packages Framework, version 20251231";
|
||||
|
||||
proc print
|
||||
data = &filesWithCodes.(drop=base build folderRef fileRef rc folderid _abort_ fileId additionalContent)
|
||||
@@ -1759,7 +1769,7 @@ data _null_;
|
||||
%end;
|
||||
put +(-1) '`.;'''
|
||||
/ ' !! '' %put The macro generated: '' !! put(dtCASLudf, E8601DT19.-L) !! ";"'
|
||||
/ ' !! '' %put with the SAS Packages Framework version 20251228.;'''
|
||||
/ ' !! '' %put with the SAS Packages Framework version 20251231.;'''
|
||||
/ ' !! '' %put ****************************************************************************;'''
|
||||
/ ' !! '' %GOTO theEndOfTheMacro;'''
|
||||
/ ' !! '' %end;''' ;
|
||||
@@ -1923,7 +1933,7 @@ data _null_;
|
||||
%end;
|
||||
put +(-1) '`.; '' !!' /
|
||||
''' %put The macro generated: ''' " !! put(dtIML, E8601DT19.-L) !! " '''; '' !! ' /
|
||||
''' %put with the SAS Packages Framework version 20251228.; '' !! ' /
|
||||
''' %put with the SAS Packages Framework version 20251231.; '' !! ' /
|
||||
''' %put ****************************************************************************; '' !! ' /
|
||||
''' %GOTO theEndOfTheMacro; '' !! ' /
|
||||
''' %end; '' !! ' /
|
||||
@@ -2726,7 +2736,7 @@ data _null_;
|
||||
%end;
|
||||
|
||||
put 'put " " / @3 "---------------------------------------------------------------------" / " ";'
|
||||
/ 'put @3 "*SAS package generated by SAS Package Framework, version `20251228`*";'
|
||||
/ 'put @3 "*SAS package generated by SAS Package Framework, version `20251231`*";'
|
||||
/ "put @3 '*under `&sysscp.`(`&sysscpl.`) operating system,*';"
|
||||
/ "put @3 '*using SAS release: `&sysvlong4.`.*';"
|
||||
/ 'put " " / @3 "---------------------------------------------------------------------";';
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
*/
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to get help about SAS package, version 20251228. Run %helpPackage() for help info.'
|
||||
des = 'Macro to get help about SAS package, version 20251231. Run %helpPackage() for help info.'
|
||||
;
|
||||
%if (%superq(packageName) = ) OR (%qupcase(&packageName.) = HELP) %then
|
||||
%do;
|
||||
@@ -43,7 +43,7 @@ des = 'Macro to get help about SAS package, version 20251228. Run %helpPackage()
|
||||
%put ### This is short help information for the `helpPackage` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to get help about SAS packages, version `20251228` #;
|
||||
%put # Macro to get help about SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*+installPackage+*/
|
||||
/* Macros to install SAS packages, version 20251228 */
|
||||
/* Macros to install SAS packages, version 20251231 */
|
||||
/* A SAS package is a zip file containing a group of files
|
||||
with SAS code (macros, functions, data steps generating
|
||||
data, etc.) wrapped up together and %INCLUDEed by
|
||||
@@ -26,7 +26,7 @@
|
||||
/secure
|
||||
minoperator
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to install SAS package, version 20251228. Run %%installPackage() for help info.'
|
||||
des = 'Macro to install SAS package, version 20251231. Run %%installPackage() for help info.'
|
||||
;
|
||||
%if (%superq(packagesNames) = ) OR (%qupcase(&packagesNames.) = HELP) %then
|
||||
%do;
|
||||
@@ -41,7 +41,7 @@ des = 'Macro to install SAS package, version 20251228. Run %%installPackage() fo
|
||||
%put ### This is short help information for the `installPackage` macro #;
|
||||
%put #--------------------------------------------------------------------------------------------#;;
|
||||
%put # #;
|
||||
%put # Macro to install SAS packages, version `20251228` #;
|
||||
%put # Macro to install SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
vERRb /* indicates if macro should be verbose and report errors */
|
||||
)
|
||||
/ minoperator PARMBUFF
|
||||
des = 'Macro to check if the PACKAGES fileref is "correct", type %isPackagesFilerefOK(HELP) for help, version 20251228.'
|
||||
des = 'Macro to check if the PACKAGES fileref is "correct", type %isPackagesFilerefOK(HELP) for help, version 20251231.'
|
||||
;
|
||||
/*** HELP END ***/
|
||||
%if %QUPCASE(&SYSPBUFF.) = %str(%(HELP%)) %then
|
||||
@@ -20,7 +20,7 @@ des = 'Macro to check if the PACKAGES fileref is "correct", type %isPackagesFile
|
||||
%put ### This is short help information for the `isPackagesFilerefOK` macro #;
|
||||
%put #-----------------------------------------------------------------------------------------#;;
|
||||
%put # #;
|
||||
%put # Macro to check if the `packages` fileref is "correct", version `20251228` #;
|
||||
%put # Macro to check if the `packages` fileref is "correct", version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Macro to list SAS packages in packages folder.
|
||||
|
||||
Version 20251228
|
||||
Version 20251231
|
||||
|
||||
A SAS package is a zip file containing a group
|
||||
of SAS codes (macros, functions, data steps generating
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
%macro listPackages()
|
||||
/secure PARMBUFF
|
||||
des = 'Macro to list SAS packages from `packages` fileref, type %listPackages(HELP) for help, version 20251228.'
|
||||
des = 'Macro to list SAS packages from `packages` fileref, type %listPackages(HELP) for help, version 20251231.'
|
||||
;
|
||||
%if %QUPCASE(&SYSPBUFF.) = %str(%(HELP%)) %then
|
||||
%do;
|
||||
@@ -38,7 +38,7 @@ des = 'Macro to list SAS packages from `packages` fileref, type %listPackages(HE
|
||||
%put ### This is short help information for the `listPackages` macro #;
|
||||
%put #-----------------------------------------------------------------------------------------#;;
|
||||
%put # #;
|
||||
%put # Macro to list available SAS packages, version `20251228` #;
|
||||
%put # Macro to list available SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
@@ -93,7 +93,7 @@ des = 'Macro to list SAS packages from `packages` fileref, type %listPackages(HE
|
||||
options NOnotes NOsource ls=MAX ps=MAX;
|
||||
|
||||
data _null_;
|
||||
length baseAll $ 32767;
|
||||
length baseAll $ 32767 base $ 1024;
|
||||
baseAll = pathname("packages");
|
||||
|
||||
if baseAll = " " then
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
*/
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to load SAS package, version 20251228. Run %loadPackage() for help info.'
|
||||
des = 'Macro to load SAS package, version 20251231. Run %loadPackage() for help info.'
|
||||
minoperator
|
||||
;
|
||||
%if (%superq(packageName) = ) OR (%qupcase(&packageName.) = HELP) %then
|
||||
@@ -52,7 +52,7 @@ minoperator
|
||||
%put ### This is short help information for the `loadPackage` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to *load* SAS packages, version `20251228` #;
|
||||
%put # Macro to *load* SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
is provided in required version */
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to load additional content for a SAS package, version 20251228. Run %loadPackageAddCnt() for help info.'
|
||||
des = 'Macro to load additional content for a SAS package, version 20251231. Run %loadPackageAddCnt() for help info.'
|
||||
minoperator
|
||||
;
|
||||
%if (%superq(packageName) = ) OR (%qupcase(&packageName.) = HELP) %then
|
||||
@@ -35,7 +35,7 @@ minoperator
|
||||
%put ### This is short help information for the `loadPackageAddCnt` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to *load* additional content for a SAS package, version `20251228` #;
|
||||
%put # Macro to *load* additional content for a SAS package, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to load multiple SAS packages at one run, version 20251228. Run %loadPackages() for help info.'
|
||||
des = 'Macro to load multiple SAS packages at one run, version 20251231. Run %loadPackages() for help info.'
|
||||
parmbuff
|
||||
;
|
||||
%if (%superq(packagesNames) = ) OR (%qupcase(&packagesNames.) = HELP) %then
|
||||
@@ -27,7 +27,7 @@ parmbuff
|
||||
%put ### This is short help information for the `loadPackageS` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro wrapper for the loadPackage macro, version `20251228` #;
|
||||
%put # Macro wrapper for the loadPackage macro, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to preview content of a SAS package, version 20251228. Run %previewPackage() for help info.'
|
||||
des = 'Macro to preview content of a SAS package, version 20251231. Run %previewPackage() for help info.'
|
||||
;
|
||||
%if (%superq(packageName) = ) OR (%qupcase(&packageName.) = HELP) %then
|
||||
%do;
|
||||
@@ -38,7 +38,7 @@ des = 'Macro to preview content of a SAS package, version 20251228. Run %preview
|
||||
%put ### This is short help information for the `previewPackage` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to get preview of a SAS packages, version `20251228` #;
|
||||
%put # Macro to get preview of a SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
,try=3 /* integer between 1 and 9 */
|
||||
,debug=0 /* debugging indicator */
|
||||
,ignorePackagesFilerefCheck=0
|
||||
,psMAX=MAX /* pageSise in case executed inside DoSubL() */
|
||||
,ods= /* a data set for results, e.g., work.relocatePackageReport */
|
||||
)
|
||||
/ des = 'Utility macro that locally Copies or Moves Packages, version 20251228. Run %relocatePackage() for help info.'
|
||||
/ des = 'Utility macro that locally Copies or Moves Packages, version 20251231. Run %relocatePackage() for help info.'
|
||||
secure
|
||||
minoperator
|
||||
;
|
||||
@@ -31,7 +33,7 @@
|
||||
%put ### This is short help information for the `relocatePackage` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to *locally copy or move* (relocate) SAS packages, version `20251228` #;
|
||||
%put # Macro to *locally copy or move* (relocate) SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
@@ -156,7 +158,7 @@
|
||||
%let msglevel_tmp = %sysfunc(getoption(msglevel));
|
||||
%let mautocomploc_tmp = %sysfunc(getoption(mautocomploc));
|
||||
|
||||
options NOnotes NOsource ls=MAX ps=MAX NOfullstimer NOstimer msglevel=N NOmautocomploc;
|
||||
options NOnotes NOsource ls=128 ps=&psMAX. NOfullstimer NOstimer msglevel=N NOmautocomploc;
|
||||
|
||||
%if NOT(%superq(debug) in (0 1)) %then %let debug=0;
|
||||
%if NOT(%superq(move) in (0 1)) %then %let move=0;
|
||||
@@ -183,8 +185,9 @@
|
||||
%put WARNING: Checksum verification impossible! Minimum SAS version required for the process is 9.4M6. ;
|
||||
%end;
|
||||
|
||||
data _null_;
|
||||
putlog 32*"*" 24*"=" 32*"*";
|
||||
data _null_ %if %superq(ods) NE %then %do; &ods. %end;
|
||||
;
|
||||
putlog 52*"*" 24*"=" 52*"*";
|
||||
length packages source target $ 32767 sDevice tDevice $ 32;
|
||||
packages = lowcase(compress(symget('packageName'),"_ ","KAD"));
|
||||
|
||||
@@ -334,7 +337,7 @@
|
||||
do i = 1 to countw(packages, " ");
|
||||
package = scan(packages, i, " ");
|
||||
|
||||
putlog 32*"*" package $24.-C 32*"*";
|
||||
putlog 52*"*" package $24.-C 52*"*";
|
||||
|
||||
select;
|
||||
/* copy from PACKAGES to some location */
|
||||
@@ -562,6 +565,7 @@
|
||||
/ "WARNING- Source is: " s_HASHING
|
||||
/ "WARNING- Target is: " t_HASHING
|
||||
/ "WARNING- There could be errors during copying. Check your files.";
|
||||
%if %superq(ods) NE %then %do; output %scan(&ods.,1,()) ; %end;
|
||||
end;
|
||||
%end;
|
||||
|
||||
@@ -600,7 +604,7 @@
|
||||
|
||||
/* LINK 3 */
|
||||
stopProcessing:
|
||||
putlog 32*"*" 24*"=" 32*"*";
|
||||
putlog 52*"*" 24*"=" 52*"*";
|
||||
stop;
|
||||
return;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ SPFmacroName /* space separated list of names */
|
||||
/
|
||||
minoperator
|
||||
secure
|
||||
des = 'Macro to provide help notes about SAS Packages Framework macros, version 20251228. Run %SasPackagesFrameworkNotes(HELP) for help info.'
|
||||
des = 'Macro to provide help notes about SAS Packages Framework macros, version 20251231. Run %SasPackagesFrameworkNotes(HELP) for help info.'
|
||||
;
|
||||
%local list N i element;
|
||||
%let list=
|
||||
@@ -28,6 +28,8 @@ splitCodeForPackage
|
||||
extendPackagesFileref
|
||||
relocatePackage
|
||||
isPackagesFilerefOK
|
||||
bundlePackages
|
||||
unbundlePackages
|
||||
/**/
|
||||
SasPackagesFrameworkNotes
|
||||
;
|
||||
@@ -49,7 +51,7 @@ SasPackagesFrameworkNotes
|
||||
%put ### This is short help information for the `SasPackagesFrameworkNotes` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro prints help notes for SAS Packages Framework macros, version `20251228` #;
|
||||
%put # Macro prints help notes for SAS Packages Framework macros, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
@@ -162,7 +164,3 @@ options mlogic symbolgen;
|
||||
*/
|
||||
|
||||
|
||||
/* end of SPFinit.sas file */
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*+SPFint_gnPckg_arch+*/
|
||||
%macro SPFint_gnPckg_arch()/secure minoperator
|
||||
des='SAS Packages Framework internal macro. Executable only inside the %generatePackage() macro. The macro encapsulates the archive version generation part of the process. Version 20251228.';
|
||||
des='SAS Packages Framework internal macro. Executable only inside the %generatePackage() macro. The macro encapsulates the archive version generation part of the process. Version 20251231.';
|
||||
/* macro picks up all macrovariables from external scope, so from the %generatePackage() macro */
|
||||
%if %sysmexecname(%sysmexecdepth-1) in (GENERATEPACKAGE) %then
|
||||
%do;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*+SPFint_gnPckg_markdown+*/
|
||||
%macro SPFint_gnPckg_markdown()/secure minoperator
|
||||
des='SAS Packages Framework internal macro. Executable only inside the %generatePackage() macro. The macro encapsulates the markdown documentation part of the process. Version 20251228.';
|
||||
des='SAS Packages Framework internal macro. Executable only inside the %generatePackage() macro. The macro encapsulates the markdown documentation part of the process. Version 20251231.';
|
||||
/* macro picks up all macrovariables from external scope, so from the %generatePackage() macro */
|
||||
%if %sysmexecname(%sysmexecdepth-1) in (GENERATEPACKAGE) %then
|
||||
%do;
|
||||
@@ -112,7 +112,7 @@ data &filesWithCodes.markdown;
|
||||
%end;
|
||||
|
||||
put " " / "---------------------------------------------------------------------" / " "
|
||||
/ "*SAS package generated by SAS Package Framework, version `20251228`,*"
|
||||
/ "*SAS package generated by SAS Package Framework, version `20251231`,*"
|
||||
/ "*under `&sysscp.`(`&sysscpl.`) operating system,*"
|
||||
/ "*using SAS release: `&sysvlong4.`.*"
|
||||
/ " " / "---------------------------------------------------------------------" / " ";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*+SPFint_gnPckg_tests+*/
|
||||
%macro SPFint_gnPckg_tests()/secure minoperator
|
||||
des='SAS Packages Framework internal macro. Executable only inside the %generatePackage() macro. The macro encapsulates the test part of the process. Version 20251228.';
|
||||
des='SAS Packages Framework internal macro. Executable only inside the %generatePackage() macro. The macro encapsulates the test part of the process. Version 20251231.';
|
||||
/* macro picks up all macrovariables from external scope, so from the %generatePackage() macro */
|
||||
%if %sysmexecname(%sysmexecdepth-1) in (GENERATEPACKAGE) %then
|
||||
%do;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
,nobs=0 /* technical parameter */
|
||||
)
|
||||
/*** HELP END ***/
|
||||
/ des = 'Utility macro to split "one big" code into multiple files for a SAS package, version 20251228. Run %splitCodeForPackage() for help info.'
|
||||
/ des = 'Utility macro to split "one big" code into multiple files for a SAS package, version 20251231. Run %splitCodeForPackage() for help info.'
|
||||
;
|
||||
%if (%superq(codeFile) = ) OR (%qupcase(&codeFile.) = HELP) %then
|
||||
%do;
|
||||
@@ -24,7 +24,7 @@
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Utility macro to *split* single file with SAS package code into multiple #;
|
||||
%put # files with separate snippets, version `20251228` #;
|
||||
%put # files with separate snippets, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
@@ -400,7 +400,7 @@ options nomprint nosymbolgen nomlogic notes source ls=MAX ps=MAX msglevel=N ;
|
||||
*/
|
||||
if firstLine[j] then
|
||||
do;
|
||||
put '/* File generated with help of SAS Packages Framework, version 20251228. */';
|
||||
put '/* File generated with help of SAS Packages Framework, version 20251231. */';
|
||||
firstLine[j]=0;
|
||||
end;
|
||||
put _infile_;
|
||||
|
||||
391
SPF/Macros/unbundlePackages.sas
Normal file
391
SPF/Macros/unbundlePackages.sas
Normal file
@@ -0,0 +1,391 @@
|
||||
/*+unbundlePackages+*/
|
||||
%macro unbundlePackages(
|
||||
bundleName
|
||||
,path=
|
||||
,pathRef=
|
||||
,packagesPath=
|
||||
,packagesRef=packages
|
||||
,ods= /* data set for report file */
|
||||
,verify=0
|
||||
)/
|
||||
des='Macro to extract a bundle of SAS packages, version 20251231. Run %unbundlePackages(HELP) for help info.'
|
||||
secure
|
||||
minoperator
|
||||
;
|
||||
|
||||
%if (%superq(bundleName) = ) OR (%qupcase(&bundleName.) = HELP) %then
|
||||
%do;
|
||||
%local options_tmp ;
|
||||
%let options_tmp = ls=%sysfunc(getoption(ls)) ps=%sysfunc(getoption(ps))
|
||||
%sysfunc(getoption(notes)) %sysfunc(getoption(source))
|
||||
msglevel=%sysfunc(getoption(msglevel))
|
||||
;
|
||||
options NOnotes NOsource ls=MAX ps=MAX msglevel=N;
|
||||
%put ;
|
||||
%put #################################################################################;
|
||||
%put ### This is short help information for the `unbundlePackages` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to *extract* SAS packages from a bundle, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
%put # data, etc.) wrapped up together and included by #;
|
||||
%put # a single `load.sas` file (also embedded inside the zip). #;
|
||||
%put # #;
|
||||
%put # The `%nrstr(%%unbundlePackages())` macro allows to extract SAS packages from #;
|
||||
%put # a bundle into a single directory. #;
|
||||
%put # #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put #### Parameters: #;
|
||||
%put # #;
|
||||
%put # 1. `bundleName` *Required.* Name of a bundle, e.g. myBundle, #;
|
||||
%put # extension `.bundle.zip` is automatically added. #;
|
||||
%put # For empty value or `HELP` this help information #;
|
||||
%put # is displayed. #;
|
||||
%put # #;
|
||||
%put # - `path=` *Required.* Location of the bundle. Must be a valid #;
|
||||
%put # directory. Takes precedence over `pathRef` parameter. #;
|
||||
%put # Either `path=` or `pathRef=` must be non-empty. #;
|
||||
%put # #;
|
||||
%put # - `pathRef=` *Optional.* Fileref to location of the bundle. #;
|
||||
%put # Either `path=` or `pathRef=` must be non-empty. #;
|
||||
%put # #;
|
||||
%put # - `packagesPath=` *Optional.* Location for packages extracted from #;
|
||||
%put # the bundle. Takes precedence over `packagesRef`. #;
|
||||
%put # When non-empty, must be a valid directory. #;
|
||||
%put # #;
|
||||
%put # - `packagesRef=` *Optional.* Fileref to location where packages will #;
|
||||
%put # be extracted. Default value is `packages`. #;
|
||||
%put # #;
|
||||
%put # - `ods=` *Optional.* Name of SAS data set for the report. #;
|
||||
%put # #;
|
||||
%put # - `verify=` *Optional.* Indicates if verification code should #;
|
||||
%put # be executed after bundle extraction. #;
|
||||
%put # Value `1` means yes, Value `0` means no. #;
|
||||
%put # #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Visit: `https://github.com/yabwon/SAS_PACKAGES/tree/main/SPF/Documentation` #;
|
||||
%put # to learn more. #;
|
||||
%put # Tutorials available at: `https://github.com/yabwon/HoW-SASPackages` #;
|
||||
%put # #;
|
||||
%put ### Example 1 ###################################################################;
|
||||
%put # #;
|
||||
%put # Enabling the SAS Package Framework #;
|
||||
%put # from the local directory and extract a bundle of #;
|
||||
%put # packages from user home directory to packages. #;
|
||||
%put # #;
|
||||
%put # Assume that the `SPFinit.sas` file #;
|
||||
%put # is located in the "/sas/PACKAGES/" folder. #;
|
||||
%put # #;
|
||||
%put # Run the following code in your SAS session: #;
|
||||
%put ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~sas;
|
||||
%put %nrstr( filename packages "/sas/PACKAGES/"; %%* setup a directory for packages;);
|
||||
%put %nrstr( %%include packages(SPFinit.sas); %%* enable the framework; );
|
||||
%put ;
|
||||
%put %nrstr( %%unbundlePackages%(myLittleBundle );
|
||||
%put %nrstr( ,path=/home/user/bundles );
|
||||
%put %nrstr( ,verify=1 );
|
||||
%put %nrstr( ,packagesRef=PACKAGES%) );
|
||||
%put ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~;
|
||||
%put # #;
|
||||
%put #################################################################################;
|
||||
%put ;
|
||||
options &options_tmp.;
|
||||
%GOTO ENDofunbundlePackages;
|
||||
%end;
|
||||
/* local variables for options */
|
||||
%local ls_tmp ps_tmp notes_tmp source_tmp stimer_tmp fullstimer_tmp msglevel_tmp mautocomploc_tmp;
|
||||
%let ls_tmp = %sysfunc(getoption(ls));
|
||||
%let ps_tmp = %sysfunc(getoption(ps));
|
||||
%let notes_tmp = %sysfunc(getoption(notes));
|
||||
%let source_tmp = %sysfunc(getoption(source));
|
||||
%let stimer_tmp = %sysfunc(getoption(stimer));
|
||||
%let fullstimer_tmp = %sysfunc(getoption(fullstimer));
|
||||
%let msglevel_tmp = %sysfunc(getoption(msglevel));
|
||||
%let mautocomploc_tmp = %sysfunc(getoption(mautocomploc));
|
||||
|
||||
options NOnotes NOsource ls=128 ps=MAX NOfullstimer NOstimer msglevel=N NOmautocomploc;
|
||||
/*===================================================================================================*/
|
||||
|
||||
%if NOT(%superq(verify) in (0 1)) %then %let verify=0;
|
||||
|
||||
%local HASHING_FILE_exist;
|
||||
%let HASHING_FILE_exist = 0;
|
||||
|
||||
%if %sysfunc(exist(sashelp.vfunc, VIEW)) %then
|
||||
%do;
|
||||
data _null_;
|
||||
set sashelp.vfunc(keep=fncname);
|
||||
where fncname = "HASHING_FILE";
|
||||
call symputX('HASHING_FILE_exist', 1, "L");
|
||||
stop;
|
||||
run;
|
||||
%end;
|
||||
|
||||
%local reportFile datetime;
|
||||
%let datetime = %sysfunc(datetime());
|
||||
%let reportFile = WORK.tmpbundlefile%sysfunc(int(&datetime.), b8601dt15.)_;
|
||||
|
||||
data _null_ ;
|
||||
datetime=symgetn('datetime');
|
||||
|
||||
length packagesList $ 32767 bundleName $ 128;
|
||||
|
||||
bundleName = compress(symget('bundleName'),"_.","KAD"); /* bundle name is letters, digits, and underscore, up to 128 symbols */
|
||||
|
||||
if bundleName NE symget('bundleName') then /* warn about illegal characters */
|
||||
do;
|
||||
put "ERROR: Bundle name contains illegal characters. Exiting";
|
||||
stop;
|
||||
end;
|
||||
|
||||
bundleName=lowcase(bundleName);
|
||||
/* if there is ".bundle.zip" extension added, remove it */
|
||||
if substr(strip(reverse(bundleName)),1,11) = 'piz.eldnub.' then bundleName=scan(bundleName,-3,".");
|
||||
else /* if there is ".bundle" extension added, remove it */
|
||||
if substr(strip(reverse(bundleName)),1,7) = 'eldnub.' then bundleName=scan(bundleName,-2,".");
|
||||
|
||||
put / "INFO: Bundle name is: " bundleName / ;
|
||||
|
||||
length packagesPath $ 32767 packagesRef $ 8;
|
||||
packagesPath = dequote(symget('packagesPath'));
|
||||
packagesRef = upcase(strip(symget('packagesRef')));
|
||||
|
||||
|
||||
/* organize target path (location for packages) */
|
||||
if " "=packagesPath then
|
||||
do;
|
||||
if 0 then set SASHELP.VEXTFL(keep=level xpath xengine fileref exists);
|
||||
DECLARE HASH sH(dataset:'SASHELP.VEXTFL(where=(fileref=' !! quote(packagesRef) !! '))', ordered: "A");
|
||||
sH.DefineKey("level");
|
||||
sH.DefineData("xpath","xengine","exists");
|
||||
sH.DefineDone();
|
||||
|
||||
if sH.NUM_ITEMS=0 then
|
||||
do;
|
||||
put "ERROR: Fileref in packagesRef= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
packagesPath=" ";
|
||||
|
||||
rc = sH.FIND(key:NOT(1=sH.NUM_ITEMS)); /* if only 1 element select level 0, if more than 1 select level 1 */
|
||||
if xengine = "DISK" AND exists='yes' then
|
||||
packagesPath=quote(strip(xpath)); /* add quotes to the packagesPath */
|
||||
else
|
||||
do;
|
||||
put "ERROR: Path: " xpath "in packagesRef= is invalid! Exiting!";
|
||||
stop;
|
||||
end;
|
||||
end;
|
||||
else
|
||||
do;
|
||||
rcPckPath = fileexist(strip(packagesPath));
|
||||
if 0=rcPckPath then
|
||||
do;
|
||||
put "ERROR: Path in packagesPath= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
else packagesPath=quote(strip(packagesPath)); /* add quotes to the packagesPath */
|
||||
end;
|
||||
|
||||
length path $ 32767 pathRef $ 8;
|
||||
path = dequote(symget('path'));
|
||||
pathRef = upcase(strip(symget('pathRef')));
|
||||
|
||||
if " "=path and " "=pathRef then
|
||||
do;
|
||||
put "ERROR: Path= and pathRef= are empty! Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
/* verify source path (location of the bundle) */
|
||||
if " "=path then
|
||||
do;
|
||||
DECLARE HASH tH(dataset:'SASHELP.VEXTFL(where=(fileref=' !! quote(pathRef) !! '))', ordered: "A");
|
||||
tH.DefineKey("level");
|
||||
tH.DefineData("xpath","xengine","exists");
|
||||
tH.DefineDone();
|
||||
DECLARE HITER tI("tH");
|
||||
|
||||
do while (tI.next()=0);
|
||||
put "Checking in: " xpath;
|
||||
if fileexist(cats(xpath,"/",bundleName,'.bundle.zip')) then
|
||||
do;
|
||||
path=strip(xpath);
|
||||
put "INFO: Bundle file " bundleName +(-1) ".bundle.zip found under: " xpath;
|
||||
leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
if " "=path then
|
||||
do;
|
||||
put "ERROR: Bundle: " bundleName "does NOT exist in any directory in pathRef=. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
end;
|
||||
else
|
||||
do;
|
||||
rcPath = fileexist(strip(path));
|
||||
if 0=rcPath then
|
||||
do;
|
||||
put "ERROR: Path in Path= does NOT exist. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
end;
|
||||
|
||||
/* get the list of packages to unbundle from bundlecontent.sas */
|
||||
length bundlecontentFR $ 8;
|
||||
rc1 = filename(bundlecontentFR, cats(path,"/",bundleName,'.bundle.zip'));
|
||||
rcE = fexist(bundlecontentFR);
|
||||
rc2 = filename(bundlecontentFR);
|
||||
|
||||
if 0=rcE then
|
||||
do;
|
||||
put "ERROR: The " bundleName "file does NOT exist!. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
length bundlecontentFR $ 8;
|
||||
rc1 = filename(bundlecontentFR, cats(path,"/",bundleName,'.bundle.zip'), 'zip', 'member="bundlecontent.sas"');
|
||||
rcE = fexist(bundlecontentFR);
|
||||
rc2 = filename(bundlecontentFR);
|
||||
|
||||
if 0=rcE then
|
||||
do;
|
||||
put "ERROR: The bundlecontent.sas file does NOT exist inside bundle. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
|
||||
length bundlecontentfile $ 1024;
|
||||
bundlecontentfile = cats(path,"/",bundleName,'.bundle.zip');
|
||||
|
||||
infile _DUMMY_ ZIP FILEVAR=bundlecontentfile member="bundlecontent.sas" end=EOF dlm=",";
|
||||
|
||||
DECLARE HASH Q(ordered:"A");
|
||||
Q.defineKey("package");
|
||||
Q.defineData("package",'pckVer','pckDtm','hash');
|
||||
Q.defineDone();
|
||||
DECLARE HITER IQ("Q");
|
||||
|
||||
/*--------------------------------------------------*/
|
||||
do until(EOF);
|
||||
input package :$32. pckVer :$16. pckDtm :$16. hash :$128.;
|
||||
if " " NE package then rc = Q.ADD();
|
||||
end;
|
||||
label package="Package name"
|
||||
pckVer="Version"
|
||||
pckDtm="Generation timestamp"
|
||||
hash="SHA256 for the Package";
|
||||
/*--------------------------------------------------*/
|
||||
|
||||
|
||||
if 0=Q.NUM_ITEMS then /* ... if empty then exit */
|
||||
do;
|
||||
put "WARNING: No packages to unbundle. Exiting!";
|
||||
stop;
|
||||
end;
|
||||
else
|
||||
do while(iQ.next()=0);
|
||||
packagesList = catx(" ", packagesList, package);
|
||||
end;
|
||||
|
||||
rc = Q.output(dataset:"&reportFile.1");
|
||||
|
||||
/* code executed for unbundling */
|
||||
length code1 code2 $ 32767;
|
||||
code1=
|
||||
'options ps=min nofullstimer nostimer msglevel=N; filename PACKAGES ' !! strip(packagesPath) !! ';' !!
|
||||
'%relocatePackage(' !! strip(packagesList) !! ',source=' !! catx("/", path, bundleName) !!
|
||||
'.bundle.zip, sDevice=ZIP,psMAX=MIN)';
|
||||
|
||||
/*put code=;*/
|
||||
|
||||
put / "INFO: The " bundleName "bundle extraction in progress...";
|
||||
|
||||
rc = doSubL(code1);
|
||||
|
||||
put / "INFO: The " bundleName "bundle extraction ended.";
|
||||
|
||||
/* code executed for verification */
|
||||
%if 1=&verify. %then
|
||||
%do;
|
||||
put / "INFO: The " bundleName "bundle verification in progress...";
|
||||
code2=
|
||||
'options ps=min nofullstimer nostimer msglevel=N; filename PACKAGES ' !! strip(packagesPath) !! ';' !!
|
||||
'filename _ ZIP ' !! quote(cats(path, "/", bundleName, ".bundle.zip")) !! ';' !!
|
||||
'%include _(verification.sas);%listPackages()';
|
||||
rc = doSubL(code2);
|
||||
put / "INFO: The " bundleName "bundle verification ended.";
|
||||
%end;
|
||||
|
||||
put " ";
|
||||
rc=sleep(1,1);
|
||||
|
||||
rc = doSubL("title 'Summary of the extracted bundle file';" !!
|
||||
"proc print data=" !!
|
||||
%if %superq(ods) NE %then
|
||||
%do; "%scan(&ods.,1,())" %end;
|
||||
%else
|
||||
%do; "&reportFile.1" %end; !!
|
||||
" label; var package pckVer pckDtm hash; run;" !!
|
||||
%if %superq(ods) NE %then
|
||||
%do; %put INFO: Report file: %scan(&ods.,1,()); %end;
|
||||
%else
|
||||
%do; "proc delete data=&reportFile.1; run;" %end; !!
|
||||
"title;");
|
||||
|
||||
stop;
|
||||
run;
|
||||
/*===================================================================================================*/
|
||||
/* restore optionos */
|
||||
options ls = &ls_tmp. ps = &ps_tmp.
|
||||
¬es_tmp. &source_tmp.
|
||||
&stimer_tmp. &fullstimer_tmp.
|
||||
msglevel=&msglevel_tmp. &mautocomploc_tmp.;
|
||||
|
||||
%ENDofunbundlePackages:
|
||||
%mend unbundlePackages;
|
||||
|
||||
/*
|
||||
options mprint;
|
||||
%unbundlePackages(
|
||||
bundlenametest123
|
||||
,path=R:\
|
||||
,packagesPath=R:\check
|
||||
,verify=1
|
||||
)
|
||||
|
||||
%unbundlePackages(
|
||||
bundlenametest124
|
||||
,path=R:\
|
||||
,packagesPath=R:\check2
|
||||
,verify=1
|
||||
)
|
||||
|
||||
%unbundlePackages(
|
||||
bundlenametest124.bundle.zip
|
||||
,path=R:\
|
||||
,packagesPath=R:\check3
|
||||
)
|
||||
|
||||
%unbundlePackages(
|
||||
bundlenametest124.bundle.zip
|
||||
,path=R:\
|
||||
,packagesPath=R:\check4
|
||||
)
|
||||
|
||||
%unbundlePackages()
|
||||
|
||||
%unbundlePackages(
|
||||
nobundlenametest123
|
||||
,path=R:\
|
||||
,packagesPath=R:\check
|
||||
,verify=1
|
||||
)
|
||||
|
||||
*/
|
||||
|
||||
/* end of SPFinit.sas file */
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to unload SAS package, version 20251228. Run %unloadPackage() for help info.'
|
||||
des = 'Macro to unload SAS package, version 20251231. Run %unloadPackage() for help info.'
|
||||
;
|
||||
%if (%superq(packageName) = ) OR (%qupcase(&packageName.) = HELP) %then
|
||||
%do;
|
||||
@@ -35,7 +35,7 @@ des = 'Macro to unload SAS package, version 20251228. Run %unloadPackage() for h
|
||||
%put ### This is short help information for the `unloadPackage` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to unload SAS packages, version `20251228` #;
|
||||
%put # Macro to unload SAS packages, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
hashing_file() function, SAS 9.4M6 */
|
||||
)/secure
|
||||
/*** HELP END ***/
|
||||
des = 'Macro to verify SAS package with the hash digest, version 20251228. Run %verifyPackage() for help info.'
|
||||
des = 'Macro to verify SAS package with the hash digest, version 20251231. Run %verifyPackage() for help info.'
|
||||
;
|
||||
%if (%superq(packageName) = ) OR (%qupcase(&packageName.) = HELP) %then
|
||||
%do;
|
||||
@@ -28,7 +28,7 @@ des = 'Macro to verify SAS package with the hash digest, version 20251228. Run %
|
||||
%put ### This is short help information for the `verifyPackage` macro #;
|
||||
%put #-------------------------------------------------------------------------------#;
|
||||
%put # #;
|
||||
%put # Macro to verify SAS package with it hash digest, version `20251228` #;
|
||||
%put # Macro to verify SAS package with it hash digest, version `20251231` #;
|
||||
%put # #;
|
||||
%put # A SAS package is a zip file containing a group #;
|
||||
%put # of SAS codes (macros, functions, data steps generating #;
|
||||
@@ -96,7 +96,7 @@ des = 'Macro to verify SAS package with the hash digest, version 20251228. Run %
|
||||
|
||||
options NOnotes NOsource ls=MAX ps=MAX NOfullstimer NOstimer msglevel=N NOmautocomploc;
|
||||
|
||||
%local _PackageFileref_;
|
||||
%local _PackageFileref_ checkExist;
|
||||
data _null_;
|
||||
call symputX("_PackageFileref_", "P" !! put(MD5(lowcase("&packageName.")), hex7. -L), "L");
|
||||
run;
|
||||
@@ -109,17 +109,18 @@ des = 'Macro to verify SAS package with the hash digest, version 20251228. Run %
|
||||
if char(packages,1) ^= "(" then packages = quote(strip(packages)); /* for paths with spaces */
|
||||
do i = 1 to kcountw(packages, "()", "QS");
|
||||
p = dequote(kscanx(packages, i, "()", "QS"));
|
||||
exists + fileexist(catx("/", p, lowcase("&packageName.") !! "zip")); /* check on zip files only! */
|
||||
exists + fileexist(catx("/", p, lowcase("&packageName.") !! ".zip")); /* check on zip files only! */
|
||||
if exists then leave;
|
||||
end;
|
||||
if exists then call symputx("path", p, "L");
|
||||
else call symputx("checkExist", '0 AND', "L");
|
||||
run;
|
||||
|
||||
filename &_PackageFileref_.
|
||||
/* put location of package myPackageFile.zip here */
|
||||
"&path./%sysfunc(lowcase(&packageName.)).zip"
|
||||
;
|
||||
%if %sysfunc(fexist(&_PackageFileref_.)) %then
|
||||
%if &checkExist. %sysfunc(fexist(&_PackageFileref_.)) %then
|
||||
%do;
|
||||
/* create hash SHA256 id *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
|
||||
%local HASHING_FILE_exist;
|
||||
@@ -141,7 +142,7 @@ des = 'Macro to verify SAS package with the hash digest, version 20251228. Run %
|
||||
filename &_PackageFileref_. list;
|
||||
|
||||
data _null_;
|
||||
length providedHash $ 100;
|
||||
length providedHash $ 128;
|
||||
providedHash = strip(symget("hash"));
|
||||
select;
|
||||
when ( 'F*' = upcase(substr(providedHash,1,2)) ) /* F = file digest */
|
||||
|
||||
Reference in New Issue
Block a user