返回索引目录

SAS Macro Source

SAS Macro 源码

每个宏保留完整源码和原始注释。使用右上角复制按钮可以复制当前宏的源码内容。

%AUC

功能:Calculate AUC and Gini statistics from Wilcoxon scores for a binary target and score variable.

使用场景:模型评估:评估二分类评分模型的区分度,输出 AUC 和 Gini 指标。

standard_sas_macro/AUC.sas
/*=====================================================================================*
Program Name     : AUC.sas
Program Owner    : Unknown
_________________________________________________________________________________________
Program Purpose  : Calculate AUC and Gini statistics from Wilcoxon scores for a binary
                   target and score variable.
Program Purpose CN: 基于二分类目标变量和评分变量的 Wilcoxon 统计量计算 AUC 与 Gini 指标。
Macro            : AUC(dsn, Target, score)
    dsn            : input scored dataset
    Target         : binary target/class variable
    score          : score or predicted probability variable
Files inputs     : SAS dataset specified by dsn
Files outputs    : WORK.AUC dataset with AUC and Gini
Notes            : Original file contains an illustrative PROC LOGISTIC demo after the macro
                   definition.
Usage Scenario   : 模型评估:评估二分类评分模型的区分度,输出 AUC 和 Gini 指标。
__________________________________________________________________________________________

Mod   Date              Name          Description
---   ---------         --------      ----------------------------------------------------
1.0   2026-06-28        Allen Sun     Standardized from input macro
*=========================================================================================*/

/*========================================================================================
Original source retained below.
========================================================================================*/
%macro AUC( dsn, Target, score);
ods select none;
ods output WilcoxonScores=WilcoxonScore;
proc npar1way wilcoxon data=&dsn ;
     where &Target^=.;
     class &Target;
     var  &score; 
run;
ods select all;

data AUC;
    set WilcoxonScore end=eof;
    retain v1 v2 1;
    if _n_=1 then v1=abs(ExpectedSum - SumOfScores);
    v2=N*v2;
    if eof then do;
       d=v1/v2;
       Gini=d * 2;    AUC=d+0.5;    
       put AUC=  GINI=;
       keep AUC Gini;
     output;
   end;
run;
%mend;

data test;
  do i = 1 to 10000;
     x = ranuni(1);
     y=(x + rannor(2315)*0.2 > 0.35 ) ; 
     output;
  end;
run;

ods select none;
ods output Association=Asso;
proc logistic data = test desc;
    model y = x;
    score data = test out = predicted ; 
run;
ods select all;

data _null_;
     set Asso;
     if Label2='c' then put 'c-stat=' nValue2;
run;
%AUC( predicted, y, p_0);

/*========================================================================================
Example usage (comment out when %include this file)
========================================================================================

data work.scored;
    input target score;
    datalines;
1 0.91
0 0.23
1 0.74
0 0.35
;
run;

%AUC(work.scored, target, score);
========================================================================================*/

%fleishman

功能:Calculate Fleishman polynomial coefficients for a requested skewness and kurtosis.

使用场景:模拟数据生成:为指定偏度和峰度计算 Fleishman 多项式系数。

standard_sas_macro/Fleishman.sas
/*=====================================================================================*
Program Name     : Fleishman.sas
Program Owner    : Unknown
_________________________________________________________________________________________
Program Purpose  : Calculate Fleishman polynomial coefficients for a requested skewness and
                   kurtosis.
Program Purpose CN: 根据给定偏度和峰度迭代计算 Fleishman 多项式系数。
Macro            : fleishman(skew, kurt, maxiter=50)
    skew           : target skewness
    kurt           : target kurtosis
    maxiter        : maximum number of iterations; default 50
Files inputs     : No input dataset required
Files outputs    : Intermediate coefficient datasets in WORK
Notes            : Temporarily changes SAS options and restores them through PROC OPTLOAD.
Usage Scenario   : 模拟数据生成:为指定偏度和峰度计算 Fleishman 多项式系数。
__________________________________________________________________________________________

Mod   Date              Name          Description
---   ---------         --------      ----------------------------------------------------
1.0   2026-06-28        Allen Sun     Standardized from input macro
*=========================================================================================*/

/*========================================================================================
Original source retained below.
========================================================================================*/

%macro fleishman(skew, kurt, maxiter=50);
/*
  Calculate Fleishman Coefficients for given Skewness and Kurtosis parameter
*/
%let converge=0.0000001;
%let X1=1; %let X2=0; %let X3=0;
proc optsave  out=_old_opt_; run;
options nomlogic nomprint nonotes;
data _coeff;
     X1=&X1; X2=&X2; X3=&X3; output;
  	 stop;
run;     
data _coeff_hist; set _coeff; run;

%do iter=1 %to &maxiter;
  data _main;
       array _F{3}  F1-F3;
  	   array _X{3}  X1-X3;
  	   retain F1-F3 X1-X3;
  	   keep F  M1-M3;
  	   if _n_=1 then do;
          set _coeff;
  	   end;
       Skewness=&Skew; Kurtosis=&Kurt;
       F=(X1**2+6*X1*X3+2*X2**2+15*X3**2-1); 
       _F[1]=abs(F);
       M1=(2*X1+6*X3); M2=(4*X2); M3=(6*X1+30*X3);
  	   output;
       F=(2*X2*(X1**2+24*X1*X3+105*X3**2+2)-SKEWNESS);  
       _F[2]=abs(F);
       M1=(4*X2*(X1+12*X3)); M2=(2*(X1**2+24*X1*X3+105*X3**2+2));
       M3=(4*X2*(12*X1+105*X3));
  	   output;
       F=(24*(X1*X3+X2**2*(1+X1**2+28*X1*X3)+X3**2*(12+48*X1*X3+141*X2**2+225*X3**2))
           -KURTOSIS);                
  	   _F[3]=abs(F);
       M1=(24*(X3+X2**2*(2*X1+28*X3)+48*X3**3));
       M2=(48*X2*(1+X1**2+28*X1*X3+141*X3**2));
       M3=(24*(X1+28*X1*X2**2+2*X3*(12+48*X1*X3+141*X2**2+225*X3**2)
           +X3**2*(48*X1+450*X3)));
  	   output;
  	   m=max(of F1-F3); put m= 7.4;
  	   if m<&converge then call symput('iter', &maxiter+1);
  run;
  %if &iter<=&maxiter %then %do;
    proc reg data=_main outest=_delta  noprint;    
         model F=M1 M2 M3/noint;
    run; quit;
    data _coeff;
         set _coeff  end=eof;     
      	 array _X{3} X1-X3;
    	   array _M{3} M1-M3;
    	   if eof then do;
    	     set _delta;
    	   end;
    	   retain M1-M3;
    	   do i=1 to 3;
    	      _X[i]=_X[i]-_M[i];
    	   end;
    	   keep X1-X3;
    run;
    data _null_; 
         set _coeff;
      	 call symput('X1', X1); call symput('X2', X2); call symput('X3', X3);
    	   stop;
    run;
    proc append data=_coeff base=_coeff_hist; run;
  %end;
%end;
proc optload data=_old_opt_; run;
%mend;

/* demo 

%fleishman(-1, 2.5);

*/

/*========================================================================================
Example usage (comment out when %include this file)
========================================================================================

%fleishman(skew=0.5, kurt=3.5, maxiter=50);
========================================================================================*/

%cal_the_stats

功能:Compute descriptive statistics for one continuous variable from any input dataset, by a split/group variable. Output 4 formatted rows (n, Mean (SD), Median, Min/Max) in wide layout (col1-colN, controlled by byvarn) with table ordering fields for TFL output workflows.

使用场景:TFL 表格生产:按治疗组或分析分组生成连续变量描述性统计行。

standard_sas_macro/cal_the_stats.sas
/*=====================================================================================*
Program Name     : cal_the_stats.sas
Program Owner    : Allen
_________________________________________________________________________________________
Program Purpose  : Compute descriptive statistics for one continuous variable
                   from any input dataset, by a split/group variable. Output 4
                   formatted rows (n, Mean (SD), Median, Min/Max) in wide layout
                    (col1-colN, controlled by byvarn) with table ordering fields for TFL output workflows.
Program Purpose CN: 按分组变量对连续型变量计算描述性统计,并输出 n、Mean (SD)、Median、Min/Max 四行宽表结果,用于 TFL 表格数据集组装。
Macro            : cal_the_stats(dsin=, where=, byvar=, byvarn=, ord=, ord1=, ord2=, val=, dsout=)
    dsin           : input dataset
    where          : WHERE clause (without WHERE keyword); blank = no filter
    byvar          : group/split variable for column layout (col1, col2, ...); must be numeric with consecutive integer values starting from 1
    byvarn         : max number of groups/columns (default 5; array size col1-col&byvarn)
    ord            : primary sort key for downstream table assembly
    ord1           : secondary sort key
    ord2           : tertiary sort key
    val            : numeric variable name to summarize
    dsout          : output dataset name
Files inputs     : SAS dataset specified by dsin (requires usubjid, &byvar, and &val)
Files outputs    : SAS dataset specified by dsout
Notes            : Supports dynamic column count via byvarn (default 5). Missing n cells set to '0'.
                    Defaults: dsin=adsl, where=fasfl='Y', byvar=grp, byvarn=5
                   Do not use BY as param name (SAS macro reserved word).
Usage Scenario   : TFL 表格生产:按治疗组或分析分组生成连续变量描述性统计行。
__________________________________________________________________________________________

Mod   Date              Name          Description
---   ---------         --------      ----------------------------------------------------
1.0   2026-06-27        Allen Sun      Created
*=========================================================================================*/

%macro cal_the_stats(
    dsin=adsl,
    where=%str(fasfl='Y'),
    byvar=grp,
    byvarn=5,
    ord=,
    ord1=,
    ord2=,
    val=,
    dsout=
);

    /* 1. Derive max decimal places from &val (capped at 3; display uses 1) */
    data df;
        retain decimal;
        %if %length(&where) %then %do;
            set &dsin(where=(&where));
        %end;
        %else %do;
            set &dsin;
        %end;
        if ^missing(&val);
        if index(strip(put(&val,best.)),'.') then do;
            decimal = length(scan(strip(put(&val,best.)),-1,'.'));
        end;
        else do;
            decimal = 0;
        end;
    run;

    proc sql noprint;
        select max(decimal) into:decimal from df;
    quit;
    %if %length(&decimal) %then %do;
        %if %eval(&decimal) > 3 %then %do;
            %let decimal=3;
        %end;
    %end;

    %let decimal=1;
    %let deci  = 12.%sysfunc(strip(&decimal));
    %let deci1 = 12.%sysfunc(strip(%eval(&decimal + 1)));
    %let deci2 = 12.%sysfunc(strip(%eval(&decimal + 2)));

    /* 2. Calc n, mean, sd, median, min, max by &byvar */
    proc sql noprint;
        create table df1 as
        select  count(distinct usubjid) as n,
                max(&val) as max,
                min(&val) as min,
                median(&val) as median,
                mean(&val) as mean,
                std(&val) as std,
                &byvar,
                "&val" as name length=200
        from df
        group by &byvar;
    quit;

    /* 3. Format stat rows; map &byvar to col1-col&byvarn */
    data df2;
        length grp_ $200.;
        set df1;
        val1 = strip(put(n,best.));
        val2 = strip(put(mean, &deci1))||" ("||strip(put(std, &deci2))||")";
        val3 = strip(put(median, &deci1));
        val4 = strip(put(min, &deci))||', '||strip(put(max, &deci));
        grp_ = 'col'||strip(put(&byvar,best.));
        keep grp_ name val:;
    run;

    proc sort data=df2;
        by name grp_;
    run;

    /* 4. Transpose to wide; attach ord/label for TFL layout */
    proc transpose data=df2 out=df3;
        by name;
        id grp_;
        var val:;
    run;

    data &dsout;
        length label $200.;
        set df3;
        ord=&ord; ord1=&ord1; ord2=&ord2;
        array cols{*} $200 col1-col&byvarn;
        if _name_ = 'val1' then label = '  n';
            else if _name_ = 'val2' then label = '  Mean (SD)';
            else if _name_ = 'val3' then label = '  Median';
            else if _name_ = 'val4' then label = '  Min, Max';
        do i = 1 to dim(cols);
            if _name_ = 'val1' and missing(cols{i}) then cols{i} = '0';
        end;
        drop _name_ _label_ i;
    run;

    /* 5. Clean up intermediate datasets */
    proc datasets nolist;
        delete df df1 df2 df3;
    quit;

%mend cal_the_stats;

/*========================================================================================
Example usage (comment out when %include this file)
========================================================================================

--- prepare ADSL subset (illustrative) ---
data adsl;
    length usubjid $20;
    usubjid='001'; fasfl='Y'; grp=1; age=45.2; output;
    usubjid='002'; fasfl='Y'; grp=1; age=52.8; output;
    usubjid='003'; fasfl='Y'; grp=2; age=38.1; output;
    usubjid='004'; fasfl='Y'; grp=2; age=61.4; output;
run;

%cal_the_stats(
    dsin   = adsl,
    where  = %str(fasfl='Y'),
    byvar  = grp,
    byvarn  = 5,
    ord    = 8,
    ord1   = 1,
    ord2   = 0,
    val    = age,
    dsout  = work.ord8
);

--- result columns: ord ord1 ord2 label name col1 col2;
    label rows: n | Mean (SD) | Median | Min, Max ---
========================================================================================*/

%drop_zero_rename

功能:Clean wide datasets with dynamic columns: drop character variables that are all '0' (excluding fixed columns), then rename remaining dynamic columns to a sequential prefix (e.g. col1, col2, ...). Used for TFL Outpus workflows.

使用场景:TFL 宽表清理:删除全 0 动态列并重编号展示列。

standard_sas_macro/drop_zero_rename.sas
/*=====================================================================================*
Program Name     : drop_zero_rename.sas
Program Owner    : Allen
_________________________________________________________________________________________
Program Purpose  : Clean wide datasets with dynamic columns: drop character variables
                   that are all '0' (excluding fixed columns), then rename remaining
                   dynamic columns to a sequential prefix (e.g. col1, col2, ...).
                   Used for TFL Outpus workflows.
Program Purpose CN: 清理包含动态列的宽表数据集,删除全为 '0' 的字符型动态列,并把保留下来的动态列按指定前缀重新编号。
Macro            : drop_zero_rename(dsin=, dsout=, fixed_vars=, prefix=col)
  dsin           : input dataset
  dsout          : output dataset
  fixed_vars     : space-delimited vars to keep as-is (never dropped or renamed)
  prefix         : rename prefix for remaining dynamic cols (default: col)
Files inputs     : SAS dataset specified by dsin
Files outputs    : SAS dataset specified by dsout
Notes            : Designed for wide TFL output datasets with dynamic display columns.
Usage Scenario   : TFL 宽表清理:删除全 0 动态列并重编号展示列。
__________________________________________________________________________________________

Mod   Date              Name          Description
---   ---------         --------      ----------------------------------------------------
1.0   2026-06-27        Allen Sun      Created
*=========================================================================================*/

%macro drop_zero_rename(dsin=, dsout=, fixed_vars=, prefix=col);
 
    /* 1. Get var list, types, order */
    proc contents data=&dsin out=_cont noprint;
    run;
    
    proc sort data=_cont;
        by varnum;
    run;

    /* 2. length sets size; retain keeps values */
    data _null_;
        set _cont end=eof;
        length all_vars  $32767;
        length all_types $32767;
        retain all_vars all_types;
        all_vars  = catx(' ', all_vars, name);
        all_types = catx(' ', all_types, put(type, best.));
        if eof then do;
            call symputx('all_vars',  all_vars);
            call symputx('all_types', all_types);
            call symputx('nvar',      _n_);
        end;
    run;

    /* 3. Drop char cols (type=2) where all rows are '0' */
    %let drop_vars = ;

    %do i = 1 %to &nvar;
        %let var   = %scan(&all_vars, &i);
        %let vtype = %scan(&all_types, &i);

        /* skip fixed cols */
        %let is_fixed = 0;
        %if %length(&fixed_vars) > 0 %then %do;
            %do j = 1 %to %sysfunc(countw(&fixed_vars));
                %if %qupcase(%scan(&fixed_vars, &j)) = %qupcase(&var) %then 
                    %let is_fixed = 1;
            %end;
        %end;

        %if &is_fixed = 0 and &vtype = 2 %then %do;
            proc sql noprint;
                select count(*) into :n_bad
                from &dsin
                where &var ne '0' or &var is null;
            quit;
            %if &n_bad = 0 %then %let drop_vars = &drop_vars &var;
        %end;
    %end;

    /* 4. Build KEEP list and RENAME pairs */
    %let keep_list   = &fixed_vars;   /* fixed cols always kept */
    %let rename_stmt = ;
    %let new_num     = 1;

    %do i = 1 %to &nvar;
        %let var = %scan(&all_vars, &i);

        /* check if dropped */
        %let is_drop = 0;
        %if %length(&drop_vars) > 0 %then %do;
            %do j = 1 %to %sysfunc(countw(&drop_vars));
                %if %qupcase(%scan(&drop_vars, &j)) = %qupcase(&var) %then 
                    %let is_drop = 1;
            %end;
        %end;

        %if &is_drop = 0 %then %do;
            %let keep_list = &keep_list &var;

            /* rename non-fixed cols only */
            %let is_fixed = 0;
            %if %length(&fixed_vars) > 0 %then %do;
                %do k = 1 %to %sysfunc(countw(&fixed_vars));
                    %if %qupcase(%scan(&fixed_vars, &k)) = %qupcase(&var) %then 
                        %let is_fixed = 1;
                %end;
            %end;

            %if &is_fixed = 0 %then %do;
                %let rename_stmt = &rename_stmt &var=&prefix.&new_num;
                %let new_num     = %eval(&new_num + 1);
            %end;
        %end;
    %end;

    /* 5. Apply KEEP/RENAME */
    %put NOTE: KEEP    = &keep_list;
    %put NOTE: RENAME  = &rename_stmt;

    data &dsout;
        set &dsin (keep=&keep_list);
        %if %length(&rename_stmt) > 0 %then %do;
            rename &rename_stmt;
        %end;
    run;

    /* 6. Cleanup temp data */
    proc datasets lib=work nolist;
        delete _cont;
    quit;

%mend drop_zero_rename;

*--- prepare input (illustrative) ---*;
data work.t_disp_scr;
    length usubjid $20 trt $10 col1-col5 $8;
    usubjid='001'; trt='A'; col1='1'; col2='0'; col3='0'; col4='2'; col5='0'; output;
    usubjid='002'; trt='B'; col1='0'; col2='0'; col3='3'; col4='0'; col5='0'; output;
run;


%drop_zero_rename(
    dsin       = work.t_disp_scr,   
    dsout      = work.t_disp_clean, 
    fixed_vars = usubjid trt,       
    prefix     = col                
);

/*========================================================================================;
* Example usage (comment out when %include this file);
*========================================================================================;

*--- prepare input (illustrative) ---*;
data work.t_disp_scr;
    length usubjid $20 trt $10 col1-col5 $8;
    usubjid='001'; trt='A'; col1='1'; col2='0'; col3='0'; col4='2'; col5='0'; output;
    usubjid='002'; trt='B'; col1='0'; col2='0'; col3='3'; col4='0'; col5='0'; output;
run;
*c2, c5 are all '0' -> dropped; c1, c3, c4 kept and renamed to col1-col3 *;

%drop_zero_rename(
    dsin       = work.t_disp_scr,   
    dsout      = work.t_disp_clean, 
    fixed_vars = usubjid trt,       
    prefix     = col                
);


*--- result: usubjid trt col1 col2 col3 (c1->col1, c3->col2, c4->col3) ---*;
*========================================================================================*/

%logparse

功能:Extract performance and resource usage statistics from a SAS log file.

使用场景:运行监控:解析 SAS 日志,汇总运行时间、CPU、I/O、内存和观测数。

standard_sas_macro/logparse.sas
/*=====================================================================================*
Program Name     : logparse.sas
Program Owner    : Unknown
_________________________________________________________________________________________
Program Purpose  : Extract performance and resource usage statistics from a SAS log file.
Program Purpose CN: 解析 SAS 日志文件,抽取运行时间、CPU、I/O、内存和观测数等性能统计信息。
Macro            : logparse(saslog, outds, system, pdsloc, append=NO)
    saslog         : SAS log file name or log member name
    outds          : output dataset for parsed log statistics
    system         : source operating system identifier; defaults to &sysscp when blank
    pdsloc         : PDS location used for MVS/OS log members
    append         : append parsed output to outds, YES or NO; default NO
Files inputs     : SAS log file specified by saslog
Files outputs    : SAS dataset specified by outds
Notes            : Contains an internal getline macro used only inside logparse.
Usage Scenario   : 运行监控:解析 SAS 日志,汇总运行时间、CPU、I/O、内存和观测数。
__________________________________________________________________________________________

Mod   Date              Name          Description
---   ---------         --------      ----------------------------------------------------
1.0   2026-06-28        Allen Sun     Standardized from input macro
*=========================================================================================*/

/*========================================================================================
Original source retained below.
========================================================================================*/
/******************************************************************
*
* The %LOGPARSE() macro extracts performance statistics from a
* SAS log file.
* 
* See the README file for instructions and syntax.
*
*******************************************************************/

%macro logparse(saslog, outds, system, pdsloc, append=NO );

   /***************************************************************
   * If no system was specified, set it to the ID of the system
   * under which this proram is running, on the assumption that
   * one kind of computer is being used for both generating and
   * analyzing the logs.
   ****************************************************************/
   %if ( &system = )  %then  %let system = &sysscp;

   /***************************************************************
   * Normalize the value of the APPEND= keyword parameter.
   ****************************************************************/
   %let append = %upcase( &append );
   %if ( &append = Y  ) %then %do;  %let append = YES;  %end;
   %if ( &append = YE ) %then %do;  %let append = YES;  %end;
   %if ( &append = N  ) %then %do;  %let append = NO;   %end;

   /***************************************************************
   * Make sure APPEND= is now YES or NO.
   ****************************************************************/
   %if ( &append ne YES ) and ( &append ne NO )  %then %do;
      %put ERROR: The value of the APPEND= parameter must be YES or NO.;
      %goto exit;
   %end;

   /***************************************************************
   * If they asked for this invocation to append its output, make
   * sure a file name was supplied.
   ****************************************************************/
   %if ( &append = YES )  %then %do;
      %if ( &outds = )  %then %do;
         %put ERROR: When append=yes is specified, an output file name;
         %put        (second parameter) must also be specified.;
         %goto exit;
      %end;
   %end;

   /***************************************************************
   * Create requested data set using performance statistics found
   * in the LOG.
   ****************************************************************/
   %if ( &append = YES )  %then %do;
      data _sas_logparse_temp_1_;
   %end;
   %else %do;
      data &outds;
   %end;

      length logfile $ 200 stepname $ 20
             line $200 upcase_Line $ 200 prevline $ 200
             portdate $ 25 keyword $ 20
             platform scp $ 50 /* UNIX needs this length */
             prevblanks blanks coloncnt reprocess
             realtime usertime systime cputime
             pageflt pagercl pageswp osvconsw osiconsw
             excsemap shrsemap consemap
             blkinput bkoutput buffio dirio
             vatime vutime rsmtime excpcnt
             tskmemd tskmemp totmemd totmemp belowmem abovemem
             obsin obsout varsout
             threads events locks pools poolsx 8;

      format realtime time12.3 usertime time12.3
             systime time12.3
             datetime datetime.
             cputime time12.3;

      label  bkoutput = 'Block Output Operations'
             blkinput = 'Block Input Operations'
             buffio = 'Buffered IO'
             consemap = 'Contended Semaphores'
             cputime = 'CPU Time'
             datetime = 'Run Date/Time'
             dirio = 'Direct IO'
             events = 'Events'
             excpcnt = 'I/O count'
             excsemap = 'Exclusive Semaphores'
             host = 'System Name'
             locks = 'Locks'
             logfile = 'Log file name'
             memused = 'Memory Used'
             osmem = 'OS Memory Used'
             obsin = 'Observations Read'
             obsout = 'Observations Written'
             osiconsw = 'Involuntary Context Switches'
             osvconsw = 'Voluntary Context Switches'
             pageflt = 'Page Faults'
             pagercl = 'Page Reclaims'
             pageswp = 'Page Swaps'
             platform = 'Platform'
             pools = 'Memory Pools Created'
             poolsx = 'Memory Pools Destroyed'
             portdate = 'SAS Port Date'
             realtime = 'Elapsed Time'
             rsmtime = 'RSM Hiperspace'
             scp = 'Operating System'
             shrsemap = 'Shared Semaphores'
             stepcnt = 'Step Number'
             stepname = 'Step Name'
             systime = 'System Time'
             threads = 'Threads'
             totmemd = 'Total Memory - data'
             totmemp = 'Total Memory - program'
             tskmemd = 'Task Memory - data'
             tskmemp = 'Task Memory - program'
             belowmem = 'Total Memory - below line'
             abovemem = 'Total Memory - above line'
             usertime = 'User Time'
             varsout = 'Variables Written'
             vatime = 'Vector Affinity'
             vutime = 'Vector Usage'
             ;

      retain line upcase_Line prevline blanks prevblanks logfile
             obsin obsout varsout;
      retain stepcnt 0
             hdrfnd 0         /* 0=no hdr yet, 1=hdr fnd, 2=hdr finish*/
             portdate
             scp
             datetime
             platform         /* defined in RELNAME routine           */
             host
             ;

      /*****************************************************************
      * Moved the DROP list down here instead of an option on the DATA
      * statement to avoid generating a syntax error if no output data
      * set name is specified on the macro invocation.
      *****************************************************************/
      drop   line prevline reprocess coloncnt inbuf pos upcase_Line
             prevblanks blanks indxFld indxLen hdrfnd keyword _I_ len;


      %let ar_bnd = 3;        /* change value when array is changed   */
      array ar_hdr (&ar_bnd) $20  _temporary_ ('PASS HEADER'
                                               'APR HEADER'
                                               'WTEST HEADER');

      %if ("&sysscp" = "OS") %then %do;
         %if (%sysfunc(fileexist(".&saslog..logs"))) %then %do;
           infile ".&saslog..logs" end=lastrec pad truncover;
         %end;
         %else %do;
           infile ".&pdsloc..logs(&saslog)" end=lastrec pad truncover;
         %end;
      %end;

      %else %do;
         infile "&saslog" lrecl=250 end=lastrec pad truncover;
      %end;

      logfile="&saslog";


      /************************************************************
      * GETLINE:
      * Read in a line from the LOG file, skipping over blank
      * lines as well as page breaks.  Page breaks can be denoted
      * by "THE SAS SYSTEM" title line, but this string can also
      * appear in a NOTE: statement, so skip the former but keep
      * the latter.  Also skip lines which begin with a number,
      * like a right-justified datetime stamp.  Ordinarily such
      * lines appear on the same line as "THE SAS SYSTEM", but if
      * the user has set the LINESIZE option to a small number,
      * they are sometimes placed on the next line.
      * Stop if we hit the end of the file.
      * Remove any carriage returns (presumably from PC runs).
      * Save off the previous and current amount of indentation
      * to detect whether we're still in a FULLSTIMER block.
      *************************************************************/
      %macro getline;
         if (lastrec = 0) then do;
            prevline = line;
            do until ( (lastrec = 1) or
                       (upcase_Line =: 'NOTE:') or
                       ((compress(line) ne '') and
                        (substr(line,1,1) ne '0c'x) and
                        (compress(scan(upcase_line,1,' '),
                                  ':0123456789') ne '') and
                        (index(upcase_Line, 'THE SAS SYSTEM') le 0)) );
               input line $char200.;
               upcase_Line = upcase(line);
            end;

            line = compress(line, '0d'x);

            prevblanks = blanks;
            blanks = length(line) - length(left(line));
         end;
         else do;
            stop;
         end;
      %mend getline;


      /************************************************************
      * Read in the very first line of the LOG.
      * All subsequent lines are read in at the bottom of the loop.
      * The algorithm is written in this way to get around a
      * problem by which SAS detects a false looping condition if
      * no INPUT statement is executed in a given DATA STEP
      * iteration.
      *************************************************************/
      if (_n_ = 1) then do;
         %getline;
      end;


      /************************************************************
      * Certain steps are preceded with NOTEs containing
      * the number of observations read and/or the number of
      * observations and variables written.  Accumulate these
      * values to be written with the next observation.
      *************************************************************/
      if (index(upcase_Line,'NOTE: THERE WERE ') and
          index(upcase_Line,' OBSERVATIONS READ FROM THE DATA SET')) then do;
         obsin + input(scan(line,4,' '), 20.);
      end;
      else if (index(upcase_Line, 'NOTE: THE DATA SET ') and
               index(upcase_Line, ' OBSERVATIONS AND ') and
               index(upcase_Line, ' VARIABLES.')) then do;
         obsout + input(scan(line,7,' '), 20.);
         varsout + input(scan(line,10,' '), 20.);
      end;


      /************************************************************
      * Process PASS HEADER information
      * Look for PASS HEADER keywords
      * Each line can contain only one keyword,
      * so quit looking when a keyword is found
      *************************************************************/
      if (line = ' ' and hdrfnd = 1) then
         hdrfnd = 2;                                /* header finished */
      if (hdrfnd = 0 and (portdate = ' ' or
                          platform = ' ' or
                          scp = ' ')) then do;
         link RELNAME;
      end;

      if (hdrfnd = 0 or hdrfnd = 1) then do;       /* check for header info */
         do _I_ = 1 to &ar_bnd;
            keyword = ar_hdr(_I_);
            /* search keyword, trim needed */
            pos = index(upcase_Line, trim(keyword));
            if pos > 0 then do;                    /* keyword found */
               len = length(keyword);
               _I_ = &ar_bnd + 1;                  /* stop looking */
            end;
         end;
         keyword = ' ';                            /* clear last search lit */
         if (pos ge 1 and pos le 5) then do;       /* must be in line begining*/
            hdrfnd = 1;
            upcase_Line = substr(upcase_Line, pos + len);
            keyword = left(scan(upcase_Line,1,'='));/* find var indicator */
            select;                                /* scan out var value */
               when (keyword = 'OS')
                  scp = scan(upcase_Line,2,"=");
               when (keyword = 'HOST')
                  host = scan(upcase_Line,2,"=");
               when (keyword = 'VER')
                  portdate = scan(upcase_Line,2,"=");
               when (keyword = 'DATE') do;
                  datetime = input(scan(upcase_Line,2,"="),datetime.);
               end;
               /* add additional variable value scans here */
               otherwise;
            end;  /* select */
         end;  /* if (pos ge 1 and pos le 5) */
      end;  /* if (hdrfnd = 0 or hdrfnd = 1) */


      /************************************************************
      * Handle MVS as a special case.
      *************************************************************/
      %if ("&system" = "MVS" or "&system" = "OS") %then %do;

         /*********************************************************
         * As soon as we detect the first statistic in the current
         * block, start reading them all in.  This is done by
         * looking for the "CPU" string in the current line.  If
         * we're dealing with MVS JCL job output, there may be
         * additional lines preceding the actual SAS log which
         * contain "CPU".  Such lines are ignored if we can't
         * parse the cputime statistic from the line using known
         * SAS log syntax.
         * If we detect the SESSION-ending CPU line, output and
         * stop.
         **********************************************************/
         if (index(line, 'CPU') gt 0) then do;
            if (index(upcase_Line,
                      "INITIALIZATION PHASE USED ") > 0) then do;
               cputime=input(scan(line,6,' '), 20.) ;
               stepname=scan(line, 3, ' ') ;
               memused=input(scan(line,10,' (K'), 20.) ;


               /***************************************************
               * Check to see if the next line contains the
               * above/below line address space NOTE.  If it
               * doesn't, make sure we reprocess the line.
               ****************************************************/
               %getline;
               %macro memline;
                  if (index(upcase_Line, 'NOTE: THE ADDRESS SPACE HAS ' ||
                                         'USED A MAXIMUM OF') gt 0) then do;
                     belowmem = input(scan(line,10,' K'), 20.);
                     abovemem = input(scan(line,15,' K'), 20.);
                  end;
                  else do;
                     reprocess = 1;
                  end;
               %mend memline;
               %memline;
            end;

            else if (index(upcase_Line, "SAS SESSION USED") > 0) then do;
               cputime=input(scan(line,6,' '), 20.) ;
               stepname=scan(line,3,' ') ;
               memused=input(scan(line,10,' (K'), 20.) ;


               /***************************************************
               * Check to see if the next line contains the
               * above/below line address space NOTE.
               * Output and stop.
               ****************************************************/
               %getline;
               %memline;

               output;
               stop;
            end;

            else if (index(upcase_Line, "CPU     TIME -") > 0) then do;
               if (index(prevline, "DATA ") > 0) then
                  stepname=scan(prevline, 3, ' ');
               else
                  stepname=scan(prevline, 4, ' ');

               inbuf = scan(line, 4, ' ');
               cputime=input(inbuf, time12.3);


               /***************************************************
               * Read in the remaining FULLSTIMER lines.  We keep
               * reading as long as the line is indented the same
               * number of characters (as FULLSTIMER blocks are).
               ****************************************************/
               %getline;
               do while (blanks = prevblanks);

                  /************************************************
                  * Parse individual statistics.
                  *************************************************/
                  if (index(upcase_Line,"ELAPSED TIME -") > 0) then do;
                     inbuf = scan(line, 4, ' ');
                     realtime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,
                                 "VECTOR AFFINITY TIME -") > 0) then do;
                     inbuf = scan(line, 5, ' ');
                     vatime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,
                                 "VECTOR USAGE    TIME -") > 0) then do;
                     inbuf = scan(line, 5, ' ');
                     vutime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,
                                 "RSM HIPERSPACE  TIME -") > 0) then do;
                     inbuf = scan(line, 5, ' ');
                     rsmtime = input(inbuf, time12.3);
                  end;
                  else if (index(upcase_Line,"EXCP COUNT") > 0) then do;
                     excpcnt = input(scan(line,4,' '), 20.);
                  end;
                  else if (index(upcase_Line,"TASK  MEMORY -") > 0) then do;
                     memused = input(scan(line,4,' K'), 20.);
                     tskmemd = input(scan(line,5,' (K'), 20.);
                     tskmemp = input(scan(line,7,' K'), 20.);
                  end;
                  else if (index(upcase_Line,"TOTAL MEMORY -") > 0) then do;
                     totmemd = input(scan(line,5,' (K'), 20.);
                     totmemp = input(scan(line,7,' K'), 20.);
                  end;

                  %getline;
               end;  /* do while (blanks = prevblanks) */


               /***************************************************
               * Check to see if the next line contains the
               * above/below line address space NOTE.  If it
               * doesn't, make sure we reprocess the line.
               ****************************************************/
               %memline;

            end;  /* else if (index(upcase_Line, "CPU     TIME -") > 0) */


            /******************************************************
            * If we at least found the CPU statistic...
            *  Output an observation corresponding to the current
            *  block of statistics.
            *  Clear out the retained variables which contain info
            *  from prior NOTE lines.
            *******************************************************/
            if (cputime ne .) then do;
               output;
               stepcnt+1;
               obsin = .;
               obsout = .;
               varsout = .;
            end;

         end;  /* if ( index(line, "CPU") > 0 ) */
      %end;  /* %if ("&system" = "MVS") */


      /************************************************************
      * All other systems share the same basic FULLSTIMER format.
      *************************************************************/
      %else %do;

         /*********************************************************
         * If we detect a line beginning "Total Elapsed Time", we
         * must be parsing a Java Test Client Application that is
         * exercising an OMR server.  If so, simply read this line
         * in as the sole statistic to be captured for this test.
         * We are currently ignoring "Elapsed Time" lines in these
         * files since there were too many of them and they were
         * making the qatrack PERFREL data sets too large.
         **********************************************************/
         if (index(line, "Total Elapsed Time:") = 1) then do;
            realtime = input(scan(line, 4, ' '), 20.);
            stepname = 'Total';
            output;
            stepcnt+1;
         end;


         /*********************************************************
         * As soon as we detect the first statistic in the current
         * block, start reading them all in.
         **********************************************************/
         else if (index(line, "real time ") > 0) then do;
            if (index(prevline, 'DATA ') > 0) then
               stepname=scan(prevline, 2, ' ');
            else
               stepname=scan(prevline, 3, ' ');


            /******************************************************
            * TIMESTAT:
            * Parses statistics that could be in time format
            * (i.e., HH:MM:SS.dd).
            * Use it to parse realtime.
            *******************************************************/
            %macro timestat(var, statword);
               inbuf = scan(line, &statword, ' ');
               coloncnt = length(inbuf) -
                          length(compress(inbuf,':'));

               if (coloncnt = 0) then
                  inbuf = '0:0:' || trim(inbuf);
               else if (coloncnt = 1) then
                  inbuf = '0:' || trim(inbuf);

               &var = input(inbuf, time12.3);
            %mend timestat;

            %timestat(realtime, 3);


            /******************************************************
            * Read in the remaining FULLSTIMER lines.  We stop
            * reading either when a line is found that isn't
            * indented (ignoring page breaks) or we reach the end
            * of the LOG.
            *******************************************************/
            %getline;
            do while (blanks = prevblanks);
               if (index(line,"user cpu time") > 0) then do;
                  %if (("&system" = "ALP") or
                       ("&system" = "VMS_AXP") or
                       ("&system" = "VMS")) %then %do;
                     %timestat(cputime, 4);
                  %end;
                  %else %do;
                     %timestat(usertime, 4);
                  %end;
               end;

               else if (index(line,"system cpu time") > 0) then do;
                  %timestat(systime, 4);
               end;

               else if (index(line,"cpu time") > 0) then do;
                     %timestat(cputime, 3);
               end;

               else if (index(line,"Semaphores") > 0) then do;
                  excsemap=input(scan(line,3," "), 20.);
                  shrsemap=input(scan(line,5," "), 20.);
                  consemap=input(scan(line,7," "), 20.);
               end;


               /***************************************************
               * GETSTAT:
               * Retrieves an integer statistic and places it in
               * the passed variable.
               ****************************************************/
               %macro getstat(text, var);
                  else if (index(line, "&text") gt 0) then do;
                     &var = input(scan(line,-1,': '), 20.);
                  end      /* do NOT put a semi-colon on this line */
               %mend getstat;

               %getstat(Page Faults, pageflt);
               %getstat(Page Reclaims, pagercl);
               %getstat(Page Swaps, pageswp);
               %getstat(Voluntary Context Switches, osvconsw);
               %getstat(Involuntary Context Switches, osiconsw);
               %getstat(Block Input Operations, blkinput);
               %getstat(Block Output Operations, bkoutput);
               %getstat(Buffered IO, buffio);
               %getstat(Direct IO, dirio);
               %getstat(Threads, threads);
               %getstat(Events, events);
               %getstat(Locks, locks);
               %getstat(Memory Pools  Created, pools);
               %getstat(Memory Pools  Destroyed, poolsx);

               else if (index(line,"OS Memory ") > 0) then do;
                  osmem = input(compress(scan(line,3,' '),'k'), 20.);
               end;

               else if (index(line,"Memory ") > 0) then do;
                  memused = input(compress(scan(line,2,' '),'k'), 20.);
               end;

               %getline;
            end;  /* do while (blanks = prevblanks) */


            /******************************************************
            * Make sure we reprocess the last line read in since
            * it is not part of the FULLSTIMER block.
            *******************************************************/
            reprocess = 1;


            /******************************************************
            * Output an observation corresponding to the current
            * block of statistics.
            * Clear out the retained variables which contain info
            * from prior NOTE lines.
            *******************************************************/
            output;
            stepcnt+1;
            obsin = .;
            obsout = .;
            varsout = .;
         end; /* else if ( index(line, "real time ") > 0 ) */
      %end;  /* %else (this is a non-MVS system) */


      /************************************************************
      * If we haven't already, read in the next line.
      *************************************************************/
      if (reprocess ne 1) then do;
         %getline;
      end;

      return;


      /************************************************************
      * Identify the port date and platform.
      *************************************************************/
      RELNAME:
         length indxFld $255;
         indxFld = "PROPRIETARY SOFTWARE PRE-PRODUCTION VERSION";
         pos = indexw(upcase_Line, indxFld);
         if (pos = 0) then do;
            indxFld = "PROPRIETARY SOFTWARE RELEASE";
            pos = indexw(upcase_Line,indxFld);
         end;
         if (pos = 0) then do;
            indxFld = "PROPRIETARY SOFTWARE VERSION";
            pos = indexw(upcase_Line,indxFld);
         end;
         if (pos > 0) then do;
            /* drop initial part of line */
            indxLen = length(trim(indxFld));
            upcase_Line = substr(upcase_Line,pos + indxLen + 1);
            portdate = compress(scan(upcase_Line,-2,' ') || ' ' ||
                                scan(upcase_Line,-1,' '), '()');
         end;
         indxFld = 'THIS SESSION IS EXECUTING ON THE';
         pos = indexw(upcase_Line, indxFld);
         if (pos > 0) then do;
            /* drop initial part of line */
            indxLen = length(trim(indxFld));
            upcase_Line = substr(upcase_Line,pos + indxLen + 1);
            line = substr(line,pos + indxLen + 1);
            /* find end of platform name */
            pos = index(upcase_Line, "PLATFORM");
            if pos > 1 then do;
               /* save original case of platform name */
               platform = substr(line, 1, pos - 1);
            end;
         end;
         indxFld = 'RUNNING ON';
         pos = indexw(upcase_Line, indxFld);
         if (pos > 0) then do;
            /* drop initial part of line */
            indxLen = length(trim(indxFld));
            upcase_Line = substr(upcase_Line,pos + indxLen + 1);
            line = substr(line,pos + indxLen + 1);
            /* find end of platform name */
            pos = index(upcase_Line, ".   ");
            if (pos le 1) then
               pos = length(trim(upcase_Line)) + 1;
            /* save original case of operating system name */
            scp = substr(line, 1, pos - 1);
         end;
      return;  /* RELNAME: */
   run;

   /***************************************************************
   * If they asked for this invocation to append its output:
   * ==> Append the file produced above to the specified file.
   * ==> Delete the temporary file
   ****************************************************************/
   %if ( &append = YES )  %then %do;
      proc datasets  lib=work  nolist;
         append  base=&outds  data=_sas_logparse_temp_1_;
         delete _sas_logparse_temp_1_;
      quit;
   %end;

%exit:  %mend logparse;

/*========================================================================================
Example usage (comment out when %include this file)
========================================================================================

%logparse(
    saslog=%str(/project/logs/program.log),
    outds=work.log_stats,
    system=,
    pdsloc=,
    append=NO
);
========================================================================================*/

%sendmail

功能:Send an email using macro parameters and/or a metadata dataset containing message text and email options.

使用场景:自动通知:通过 SAS 邮件引擎发送任务结果、附件或运行状态提醒。

standard_sas_macro/sendmail.sas
/*=====================================================================================*
Program Name     : sendmail.sas
Program Owner    : Scott Bass
_________________________________________________________________________________________
Program Purpose  : Send an email using macro parameters and/or a metadata dataset containing
                   message text and email options.
Program Purpose CN: 根据宏参数或邮件元数据集发送邮件,支持收件人、抄送、主题、附件和正文内容。
Macro            : sendmail(METADATA=mail, TO=, CC=, BCC=, FROM=, REPLYTO=, SUBJECT=, ATTACH=, CONTENT_TYPE=, ENCODING=)
    METADATA       : metadata dataset containing email text and optional parameters; default mail
    TO             : To addressee list
    CC             : Cc addressee list
    BCC            : Bcc addressee list
    FROM           : From addressee
    REPLYTO        : Reply-to addressee
    SUBJECT        : email subject
    ATTACH         : attachment list
    CONTENT_TYPE   : email content type
    ENCODING       : email encoding
Files inputs     : Metadata dataset specified by METADATA, unless METADATA=_null_
Files outputs    : Email sent through the SAS EMAIL filename engine
Macros called    : parmv, seplist
Notes            : Metadata dataset parameters take precedence over macro parameters when
                   both are supplied.
Usage Scenario   : 自动通知:通过 SAS 邮件引擎发送任务结果、附件或运行状态提醒。
__________________________________________________________________________________________

Mod   Date              Name          Description
---   ---------         --------      ----------------------------------------------------
1.0   2026-06-28        Allen Sun     Standardized from input macro
*=========================================================================================*/

/*========================================================================================
Original source retained below.
========================================================================================*/
/*=====================================================================
Program Name            : sendmail.sas
Purpose                 : Macro to send an email using a metadata dataset
SAS Version             : SAS 9.1.3
Input Data              : Dataset containing email text
Output Data             : Email sent to desired recipients

Macros Called           : parmv, seplist

Originally Written by   : Scott Bass
Date                    : 12MAR2008
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 22NOV2013
Change/reason           : Changed to make email dataset optional in
                          order to send "subject only" emails.
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

* specify message only, other options are macro parameters ;
data mail;
   length parm $15 line $1000;
   infile datalines dsd dlm="|" truncover;
   input parm line;
   datalines;
| Dear Support
|
| Job foo failed.
| See bar for details.
| Test of &sysprocessid..
;
run;

%sendmail(
  to=&sysuserid
  ,subject=%str(This is the subject as of &sysdate and &systime)
)

=======================================================================

* specify all parameters in the metadata dataset ;
%let job=My Job;
data mail;
   length parm $15 line $1000;
   infile datalines dsd dlm="|" truncover;
   input parm line;
   datalines;
TO       | jack@sas.com^jill@sas.com
CC       | Joe User <joe@sas.com>
CC       | Jane User <jane@sas.com>
FROM     | Real Name <real.name@sas.com>
SUBJECT  | Job &job failed at %sysfunc(date(),date7.) %sysfunc(time(),time5.)
ATTACH   | "%sysfunc(pathname(fileref_to_error_log))"
         | Dear Support
         |
         | Job foo failed.
         | See bar for details.
         | Date &sysdate Time &systime..
;
run;

%sendmail;

=======================================================================

* send a "subject only" email without any email message ;
%sendmail(
  metadata=_null_
  ,to=&sysuserid
  ,subject=%str(This is the subject as of &sysdate and &systime)
)

-----------------------------------------------------------------------
Notes:

The end user is responsible for specifying correct parameters for the
macro.  Little error checking is done.

See http://support.sas.com/onlinedoc/913/getDoc/en/lrdict.hlp/a002058232.htm
for documentation on parameter syntax.

To send a "subject only" email with no email message, specify
METADATA=_null_ for the metadata dataset parameter.

Parameters can be specified as macro parameters, or included in the
metadata dataset.  If included in the metadata dataset, the names
must match the macro variables in the macro parameters.  The names are
case insensitive.  If both macro parameter and metadata parameters are
specified, the metadata parameters take precedence.

To, Cc, and Bcc parameters should be separated with a caret (^), since
they can contain spaces.  Do not add any syntax (eg. quotation marks),
the macro will take care of this.

To, Cc, Bcc, and Attach can be specified all on one line, or on separate lines.
If on separate lines, they will be concatenated into the appropriate
addressee or attachment list.

For the remaining parameters, if specified multiple times, the last
parameter specified will be used.

At a minimum, the text of the email must be included in the metadata
dataset.  The parm variable should be missing for the text of the email.
Make sure the line variable is long enough to contain *resolved* macro
variable references without data truncation.

Macro variable references in the metadata dataset are resolved at macro
run time.
---------------------------------------------------------------------*/

%macro sendmail
/*---------------------------------------------------------------------
Macro to send an email using a metadata dataset
---------------------------------------------------------------------*/
(METADATA=mail /* Metadata dataset (REQ).                            */
,TO=           /* To addressee (Opt).                                */
,CC=           /* Cc addressee (Opt).                                */
,BCC=          /* Bcc addressee (Opt).                               */
,FROM=
               /* From addressee (Opt).                              */
,REPLYTO=      /* Reply-to addressee (Opt).                          */
,SUBJECT=      /* Subject (Opt).                                     */
,ATTACH=       /* Attachment (Opt).                                  */
,CONTENT_TYPE= /* Content type (Opt).                                */
,ENCODING=     /* Encoding (Opt).                                    */
);

%local macro parmerr metadata_exists;

%* check input parameters ;
%let macro = &sysmacroname;
%parmv(METADATA,     _req=1,_words=0,_case=U)

%if (&parmerr) %then %goto quit;

%let metadata_exists=%sysfunc(exist(&metadata));

%* create macro variables from the metadata dataset, ;
%* overwriting any parameters passed in as macro parameters ;
%let options=%sysfunc(getoption(serror));
%if (&metadata_exists) %then %do;
  options noserror;
  data _null_;
     set &metadata end=eof;
     where parm is not missing;
     length to cc bcc attach $10000;  /* adjust length as desired, but make long enough for resolved values */
     retain to cc bcc attach;
     select(upcase(parm));
        when("TO")     to       = catx("^",to,       line);
        when("CC")     cc       = catx("^",cc,       line);
        when("BCC")    bcc      = catx("^",bcc,      line);
        when("ATTACH") attach   = catx("^",attach,   line);
        when("FROM","REPLYTO","SUBJECT","CONTENT_TYPE","ENCODING")
           call symputx(parm,line);
        otherwise do;
           put "ERR" "OR: " parm "is an invalid parameter.";
           call symputx("parmerr",1);
           stop;
        end;
     end;
     if eof then do;
        if length(to)     then call symputx("TO",     to);
        if length(cc)     then call symputx("CC",     cc);
        if length(bcc)    then call symputx("BCC",    bcc);
        if length(attach) then call symputx("ATTACH", attach);
     end;
  run;
%end;

%if (&parmerr) %then %goto quit;

%* error checking ;
%parmv(TO,           _req=1,_words=1,_case=N)

%if (&parmerr) %then %goto quit;

%* issue filename statement ;
filename email email
   %let temp = %seplist(&to,nest=QQ,indlm=^,dlm=%str( ));
   to=(&temp)

%if (%superq(cc) ne ) %then %do;
   %let temp = %seplist(&cc,nest=QQ,indlm=^,dlm=%str( ));
   cc=(&temp)
%end;

%if (%superq(bcc) ne ) %then %do;
   %let temp = %seplist(&bcc,nest=QQ,indlm=^,dlm=%str( ));
   bcc=(&temp)
%end;

%if (%superq(subject) ne ) %then %do;
   subject="&subject"
%end;

%if (%superq(from) ne ) %then %do;
   from="&from"
%end;

%if (%superq(replyto) ne ) %then %do;
   replyto="&replyto"
%end;

%if (%superq(attach) ne ) %then %do;
   %let temp = %seplist(&attach,nest=QQ,indlm=^,dlm=%str( ));
   attach=(&temp)
%end;

%if (%superq(content_type) ne ) %then %do;
   content_type="&content_type"
%end;

%if (%superq(encoding) ne ) %then %do;
   encoding="&encoding"
%end;
;

%* now send the email ;
data _null_;
   file email;
/*   file print; */  /* for debugging */
   %if (&metadata_exists) %then %do;
   set &metadata;
   where parm is missing;
   line = resolve(line);
   put line;
   %end;
run;

filename email clear;
options &options;

%quit:
%mend;

/******* END OF FILE *******/

/*========================================================================================
Example usage (comment out when %include this file)
========================================================================================

%sendmail(
    TO=%str(team@example.com),
    FROM=%str(no-reply@example.com),
    SUBJECT=%str(SAS job finished),
    CONTENT_TYPE=text/plain
);
========================================================================================*/
返回索引目录