From 872353dc8d6be557a0a32b2a6e3a7832cd37b23c Mon Sep 17 00:00:00 2001
From: 4gl <@>
Date: Thu, 14 May 2026 15:02:14 +0100
Subject: [PATCH 1/2] feat: new mx_append2pgm macro
---
tests/x-platform/mx_append2pgm.test.sas | 192 ++++++++++++++++++++++++
xplatform/mx_append2pgm.sas | 149 ++++++++++++++++++
2 files changed, 341 insertions(+)
create mode 100644 tests/x-platform/mx_append2pgm.test.sas
create mode 100644 xplatform/mx_append2pgm.sas
diff --git a/tests/x-platform/mx_append2pgm.test.sas b/tests/x-platform/mx_append2pgm.test.sas
new file mode 100644
index 0000000..3ef1360
--- /dev/null
+++ b/tests/x-platform/mx_append2pgm.test.sas
@@ -0,0 +1,192 @@
+/**
+ @file
+ @brief Testing mx_append2pgm.sas macro
+
+ Be sure to run %let mcTestAppLoc=/Public/temp/macrocore; when
+ running in Studio
+
+
SAS Macros
+ @li mf_getplatform.sas
+ @li mf_uid.sas
+ @li mp_assert.sas
+ @li mp_assertscope.sas
+ @li ms_createfile.sas
+ @li mv_createfile.sas
+ @li mm_createstp.sas
+ @li mx_append2pgm.sas
+ @li mx_getcode.sas
+
+**/
+
+/**
+ * Test 1 - Append content to an existing program and verify combined output
+ * Also checking for scope leakage
+ */
+
+/* create a unique name for the program */
+%let item=test_%mf_uid();
+
+/* create the initial program with some code */
+filename initpgm temp;
+data _null_;
+ file initpgm;
+ put '%put ORIGINAL LINE;';
+run;
+
+%macro setup_pgm();
+ %let platform=%mf_getplatform();
+ %if &platform=SASJS %then %do;
+ %ms_createfile(&mcTestAppLoc/temp/&item..sas, inref=initpgm)
+ %end;
+ %else %if &platform=SASVIYA %then %do;
+ %mv_createfile(path=&mcTestAppLoc/temp, name=&item..sas, inref=initpgm)
+ %end;
+ %else %do;
+ %let work=%sysfunc(pathname(work));
+ data _null_;
+ file "&work/&item..sas";
+ infile initpgm;
+ input;
+ put _infile_;
+ run;
+ %mm_createstp(stpname=&item
+ ,filename=&item..sas
+ ,directory=&work
+ ,tree=&mcTestAppLoc/temp
+ ,stptype=2
+ ,minify=NO
+ )
+ %end;
+%mend setup_pgm;
+%setup_pgm()
+
+/* create the content to append */
+filename toappnd temp;
+data _null_;
+ file toappnd;
+ put '%put APPENDED LINE;';
+run;
+
+/* run the macro under test with scope checks */
+%mp_assertscope(SNAPSHOT)
+%mx_append2pgm(&mcTestAppLoc/temp/&item, inref=toappnd)
+%mp_assertscope(COMPARE,
+ desc=Test 1: mx_append2pgm does not leak scope,
+ outds=work.test_results,
+ ignorelist=MC2_JADP1LEN MC2_JADP2LEN MC2_JADPNUM MC2_JADVLEN MC2_JADP3LEN
+)
+
+%mp_assert(
+ iftrue=(&syscc=0),
+ desc=Test 1: No errors after appending content to program,
+ outds=work.test_results
+)
+
+/**
+ * Test 2 - Verify the appended content is present
+ * Fetch the modified program and check both original and appended lines exist
+ */
+
+%let test2_orig=0;
+%let test2_appd=0;
+
+%macro verify_test2();
+ %let platform=%mf_getplatform();
+ %if &platform=SASVIYA %then %do;
+ filename verifrf filesrvc folderpath="&mcTestAppLoc/temp";
+ data _null_;
+ infile verifrf("&item..sas") lrecl=32000;
+ input;
+ if index(_infile_,'ORIGINAL LINE') then call symputx('test2_orig','1');
+ if index(_infile_,'APPENDED LINE') then call symputx('test2_appd','1');
+ run;
+ filename verifrf clear;
+ %end;
+ %else %do;
+ %mx_getcode(&mcTestAppLoc/temp/&item, outref=verifrf)
+ data _null_;
+ infile verifrf lrecl=32000;
+ input;
+ if index(_infile_,'ORIGINAL LINE') then call symputx('test2_orig','1');
+ if index(_infile_,'APPENDED LINE') then call symputx('test2_appd','1');
+ run;
+ %end;
+%mend verify_test2;
+%verify_test2()
+
+%mp_assert(
+ iftrue=(&test2_orig=1),
+ desc=Test 2a: Original content is preserved after append,
+ outds=work.test_results
+)
+
+%mp_assert(
+ iftrue=(&test2_appd=1),
+ desc=Test 2b: Appended content is present in modified program,
+ outds=work.test_results
+)
+
+/**
+ * Test 3 - Append multiple times to ensure repeated appends work
+ */
+filename toappd2 temp;
+data _null_;
+ file toappd2;
+ put '%put SECOND APPEND;';
+run;
+
+%mp_assertscope(SNAPSHOT)
+%mx_append2pgm(&mcTestAppLoc/temp/&item, inref=toappd2)
+%mp_assertscope(COMPARE,
+ desc=Test 3: mx_append2pgm does not leak scope on second call,
+ outds=work.test_results
+)
+
+/* verify all three pieces of content exist */
+%let test3_orig=0;
+%let test3_appd=0;
+%let test3_app2=0;
+
+%macro verify_test3();
+ %let platform=%mf_getplatform();
+ %if &platform=SASVIYA %then %do;
+ filename verifr2 filesrvc folderpath="&mcTestAppLoc/temp";
+ data _null_;
+ infile verifr2("&item..sas") lrecl=32000;
+ input;
+ if index(_infile_,'ORIGINAL LINE') then call symputx('test3_orig','1');
+ if index(_infile_,'APPENDED LINE') then call symputx('test3_appd','1');
+ if index(_infile_,'SECOND APPEND') then call symputx('test3_app2','1');
+ run;
+ filename verifr2 clear;
+ %end;
+ %else %do;
+ %mx_getcode(&mcTestAppLoc/temp/&item, outref=verifr2)
+ data _null_;
+ infile verifr2 lrecl=32000;
+ input;
+ if index(_infile_,'ORIGINAL LINE') then call symputx('test3_orig','1');
+ if index(_infile_,'APPENDED LINE') then call symputx('test3_appd','1');
+ if index(_infile_,'SECOND APPEND') then call symputx('test3_app2','1');
+ run;
+ %end;
+%mend verify_test3;
+%verify_test3()
+
+%mp_assert(
+ iftrue=(&test3_orig=1),
+ desc=Test 3a: Original content still present after second append,
+ outds=work.test_results
+)
+
+%mp_assert(
+ iftrue=(&test3_appd=1),
+ desc=Test 3b: First appended content still present after second append,
+ outds=work.test_results
+)
+
+%mp_assert(
+ iftrue=(&test3_app2=1),
+ desc=Test 3c: Second appended content is present,
+ outds=work.test_results
+)
diff --git a/xplatform/mx_append2pgm.sas b/xplatform/mx_append2pgm.sas
new file mode 100644
index 0000000..65e21d2
--- /dev/null
+++ b/xplatform/mx_append2pgm.sas
@@ -0,0 +1,149 @@
+/**
+ @file
+ @brief Appends a text file to a SASjs Stored Program, Viya SAS program, or
+ SAS 9 Stored Process
+ @details Extracts the source code from a SASjs Stored Program, Viya SAS
+ program (file in SAS Drive), or SAS 9 Stored Process, appends the contents
+ of a provided text file, then deletes and recreates the target item with the
+ combined content.
+
+ This is useful for dynamically modifying deployed programs, for example to
+ add test-specific configuration or runtime settings.
+
+ Usage:
+
+ %* compile macros ;
+ filename mc url
+ "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
+ %inc mc;
+
+ %* write some content to append;
+ filename append temp;
+ data _null_;
+ file append;
+ put "libname mylib '/some/path';";
+ run;
+
+ %* append to existing program;
+ %mx_append2pgm(/Public/app/common/settings, inref=append)
+
+ @param [in] loc The full path to the Viya SAS program, SAS 9 Stored Process,
+ or SASjs Stored Program in Drive or Metadata, WITHOUT the .sas extension
+ (SASjs only)
+ @param [in] inref= (0) Fileref pointing to the content to be appended to the
+ target program.
+ @param [in] mdebug= (0) Set to 1 to show debug messages in the log
+
+ SAS Macros
+ @li mf_getplatform.sas
+ @li mf_getuniquefileref.sas
+ @li mm_createstp.sas
+ @li mm_deletestp.sas
+ @li mm_getstpcode.sas
+ @li ms_createfile.sas
+ @li ms_deletefile.sas
+ @li mv_createfile.sas
+ @li mv_deletefoldermember.sas
+ @li mx_getcode.sas
+
+ Related Macros
+ @li mx_append2pgm.test.sas
+ @li mx_getcode.sas
+ @li mx_createjob.sas
+
+ @author Allan Bowe
+
+**/
+
+%macro mx_append2pgm(loc
+ ,inref=0
+ ,mdebug=0
+)/*/STORE SOURCE*/;
+
+%local platform name shortloc coderef combref work tmpfile viyaref;
+%let platform=%mf_getplatform();
+
+%if &mdebug=1 %then %do;
+ %put &sysmacroname entry vars:;
+ %put _local_;
+%end;
+%if &syscc ne 0 %then %do;
+ %put syscc=&syscc - &sysmacroname will not execute in this state;
+ %return;
+%end;
+
+/* extract name & path from loc */
+data _null_;
+ length name shortloc $500;
+ loc=symget('loc');
+ name=scan(loc,-1,'/');
+ shortloc=substr(loc,1,length(loc)-length(name)-1);
+ call symputx('name',name,'l');
+ call symputx('shortloc',shortloc,'l');
+run;
+
+/* create a combined fileref with original + appended content */
+%let combref=%mf_getuniquefileref();
+%let work=%sysfunc(pathname(work));
+%let tmpfile=&combref..sas;
+filename &combref "&work/&tmpfile" lrecl=32000;
+
+%if &platform=SASVIYA %then %do;
+ /* On Viya, read the SAS program file from SAS Drive using filesrvc */
+ %let viyaref=%mf_getuniquefileref();
+ filename &viyaref filesrvc folderpath="&shortloc";
+ data _null_;
+ file &combref lrecl=32000 termstr=crlf;
+ infile &viyaref("&name..sas") lrecl=32000 end=last;
+ input;
+ put _infile_;
+ run;
+ filename &viyaref clear;
+ %symdel _FILESRVC_&viyaref._URI;
+%end;
+%else %do;
+ /* For SAS9 and SASJS, use mx_getcode */
+ %let coderef=%mf_getuniquefileref();
+ %mx_getcode(&loc, outref=&coderef)
+ data _null_;
+ file &combref lrecl=32000 termstr=crlf;
+ infile &coderef lrecl=32000 end=last;
+ input;
+ put _infile_;
+ run;
+ filename &coderef clear;
+%end;
+
+/* append the new content */
+data _null_;
+ file &combref lrecl=32000 termstr=crlf mod;
+ infile &inref lrecl=32000;
+ input;
+ put _infile_;
+run;
+
+/* delete and recreate the target item */
+%if &platform=SASJS %then %do;
+ %ms_deletefile(&loc..sas)
+ %ms_createfile(&loc..sas, inref=&combref, mdebug=&mdebug)
+%end;
+%else %if &platform=SASVIYA %then %do;
+ %mv_deletefoldermember(path=&shortloc, name=&name..sas, contenttype=file)
+ %mv_createfile(path=&shortloc, name=&name..sas, inref=&combref)
+%end;
+%else %do;
+ /* SAS 9 */
+ %mm_deletestp(target=&loc)
+ %mm_createstp(stpname=&name
+ ,filename=&tmpfile
+ ,directory=&work
+ ,tree=&shortloc
+ ,stptype=2
+ ,mDebug=&mdebug
+ ,minify=NO
+ )
+%end;
+
+filename &combref clear;
+
+%mend mx_append2pgm;
From c8e1126a4072ae17fa13f1b011323bc4c328668e Mon Sep 17 00:00:00 2001
From: github-actions
Date: Thu, 14 May 2026 14:02:39 +0000
Subject: [PATCH 2/2] chore: updating all.sas
---
all.sas | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 149 insertions(+)
diff --git a/all.sas b/all.sas
index 7435be1..b3d8a0e 100644
--- a/all.sas
+++ b/all.sas
@@ -31469,6 +31469,155 @@ endsub;
%end;
%mend mcf_string2file;/**
+ @file
+ @brief Appends a text file to a SASjs Stored Program, Viya SAS program, or
+ SAS 9 Stored Process
+ @details Extracts the source code from a SASjs Stored Program, Viya SAS
+ program (file in SAS Drive), or SAS 9 Stored Process, appends the contents
+ of a provided text file, then deletes and recreates the target item with the
+ combined content.
+
+ This is useful for dynamically modifying deployed programs, for example to
+ add test-specific configuration or runtime settings.
+
+ Usage:
+
+ %* compile macros ;
+ filename mc url
+ "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
+ %inc mc;
+
+ %* write some content to append;
+ filename append temp;
+ data _null_;
+ file append;
+ put "libname mylib '/some/path';";
+ run;
+
+ %* append to existing program;
+ %mx_append2pgm(/Public/app/common/settings, inref=append)
+
+ @param [in] loc The full path to the Viya SAS program, SAS 9 Stored Process,
+ or SASjs Stored Program in Drive or Metadata, WITHOUT the .sas extension
+ (SASjs only)
+ @param [in] inref= (0) Fileref pointing to the content to be appended to the
+ target program.
+ @param [in] mdebug= (0) Set to 1 to show debug messages in the log
+
+ SAS Macros
+ @li mf_getplatform.sas
+ @li mf_getuniquefileref.sas
+ @li mm_createstp.sas
+ @li mm_deletestp.sas
+ @li mm_getstpcode.sas
+ @li ms_createfile.sas
+ @li ms_deletefile.sas
+ @li mv_createfile.sas
+ @li mv_deletefoldermember.sas
+ @li mx_getcode.sas
+
+ Related Macros
+ @li mx_append2pgm.test.sas
+ @li mx_getcode.sas
+ @li mx_createjob.sas
+
+ @author Allan Bowe
+
+**/
+
+%macro mx_append2pgm(loc
+ ,inref=0
+ ,mdebug=0
+)/*/STORE SOURCE*/;
+
+%local platform name shortloc coderef combref work tmpfile viyaref;
+%let platform=%mf_getplatform();
+
+%if &mdebug=1 %then %do;
+ %put &sysmacroname entry vars:;
+ %put _local_;
+%end;
+%if &syscc ne 0 %then %do;
+ %put syscc=&syscc - &sysmacroname will not execute in this state;
+ %return;
+%end;
+
+/* extract name & path from loc */
+data _null_;
+ length name shortloc $500;
+ loc=symget('loc');
+ name=scan(loc,-1,'/');
+ shortloc=substr(loc,1,length(loc)-length(name)-1);
+ call symputx('name',name,'l');
+ call symputx('shortloc',shortloc,'l');
+run;
+
+/* create a combined fileref with original + appended content */
+%let combref=%mf_getuniquefileref();
+%let work=%sysfunc(pathname(work));
+%let tmpfile=&combref..sas;
+filename &combref "&work/&tmpfile" lrecl=32000;
+
+%if &platform=SASVIYA %then %do;
+ /* On Viya, read the SAS program file from SAS Drive using filesrvc */
+ %let viyaref=%mf_getuniquefileref();
+ filename &viyaref filesrvc folderpath="&shortloc";
+ data _null_;
+ file &combref lrecl=32000 termstr=crlf;
+ infile &viyaref("&name..sas") lrecl=32000 end=last;
+ input;
+ put _infile_;
+ run;
+ filename &viyaref clear;
+ %symdel _FILESRVC_&viyaref._URI;
+%end;
+%else %do;
+ /* For SAS9 and SASJS, use mx_getcode */
+ %let coderef=%mf_getuniquefileref();
+ %mx_getcode(&loc, outref=&coderef)
+ data _null_;
+ file &combref lrecl=32000 termstr=crlf;
+ infile &coderef lrecl=32000 end=last;
+ input;
+ put _infile_;
+ run;
+ filename &coderef clear;
+%end;
+
+/* append the new content */
+data _null_;
+ file &combref lrecl=32000 termstr=crlf mod;
+ infile &inref lrecl=32000;
+ input;
+ put _infile_;
+run;
+
+/* delete and recreate the target item */
+%if &platform=SASJS %then %do;
+ %ms_deletefile(&loc..sas)
+ %ms_createfile(&loc..sas, inref=&combref, mdebug=&mdebug)
+%end;
+%else %if &platform=SASVIYA %then %do;
+ %mv_deletefoldermember(path=&shortloc, name=&name..sas, contenttype=file)
+ %mv_createfile(path=&shortloc, name=&name..sas, inref=&combref)
+%end;
+%else %do;
+ /* SAS 9 */
+ %mm_deletestp(target=&loc)
+ %mm_createstp(stpname=&name
+ ,filename=&tmpfile
+ ,directory=&work
+ ,tree=&shortloc
+ ,stptype=2
+ ,mDebug=&mdebug
+ ,minify=NO
+ )
+%end;
+
+filename &combref clear;
+
+%mend mx_append2pgm;
+/**
@file mx_createjob.sas
@brief Create a job in SAS 9, Viya or SASjs
@details Creates a Stored Process in SAS 9, a Job Execution Service in SAS