Download ~sas2xls.sas ~sas2xls.sasSubmit a comment

%* Deprecated;
Test code can be found here.
%macro sas2xls (
  file=
, fileref=
, sheet1=,  sheet2=,  sheet3=,  sheet4=,  sheet5=,  sheet6=,  sheet7=,  sheet8=,  sheet9=,  sheet10=
, sheet11=, sheet12=, sheet13=, sheet14=, sheet15=, sheet16=, sheet17=, sheet18=, sheet19=, sheet20=
, sheet21=, sheet22=, sheet23=, sheet24=, sheet25=, sheet26=, sheet27=, sheet28=, sheet29=, sheet30=
, sheet31=, sheet32=, sheet33=, sheet34=, sheet35=, sheet36=, sheet37=, sheet38=, sheet39=, sheet40=
, sheet41=, sheet42=, sheet43=, sheet44=, sheet45=, sheet46=, sheet47=, sheet48=, sheet49=, sheet50=
, perl= perl -w
, tempdir= %sysfunc (pathname(WORK))
);

  %*
  %* sas2xls - Use Perl to create an excel file containing one worksheet per SAS dataset
  %*
  %* Richard A. DeVenezia
  %* 5/20/02
  %*
  %* This macro does not attempt to handle formatting issues.
  %* We will have to wait for SAS to get off their duff and write an
  %* ODS EXCEL destination.
  %*
  %* Perl Host requirements:
  %*   Perl 5.005 (or later)
  %*   Spreadsheet::WriteExcel
  %*   Parse::RecDescent
  %*   File::Temp
  %*
  %* Assumptions:
  %*   Perl and SAS are installed on same host
  %*   The macro can be tweaked to run Perl on a remote host.  Your SAS host
  %*   will need the ability to remote shell to the foreign host, as well as
  %*   send and receive stdio. Look for %***% comments
  %*
  %* Arguments:
  %*   All arguments are comma separated.
  %*   The first null argument is presumed to be the last arguement, thus
  %*   if you pass a null argument, all subsequent arguments are ignored
  %*
  %* Argument categories and special considerations:
  %*
  %*-----
  %* Excel output
  %*
  %*   FILE=<host-path>
  %*   FILEREF=<SAS-fileref>  FILEREF takes precedence if FILE is also present
  %*
  %*   If fileref _WEBOUT is passed, the proper content-type will be prepended
  %*   to the generated excel file
  %*
  %*-----
  %* Worksheet names and content source
  %*
  %*  Form1: SHEET<n>=[~][<SheetName>]:[<Heading>]:<Dataset>|<CatalogEntry>
  %*  Form2: SHEET<n>=<Dataset>|<CatalogEntry>
  %*
  %*  <n> can range from 1 to 50
  %*
  %*  Form 1:
  %*
  %*   [~][<SheetName>]:[<Heading>]:<Dataset>|<CatalogEntry>
  %*
  %*     ~ is optional
  %*
  %*       A special formatting indicator
  %*       If SheetName starts with tilde (~) the worksheet is formatted
  %*       to hide grid lines and all cells are Courier New font
  %*       Use this feature when transferring a PROCs text output to Excel.
  %*       A PROCs text output can be captured to a catalog entry by using Proc PRINTTO.
  %*
  %*     <SheetName> is optional
  %*
  %*       The worksheet name.
  %*       If not present, the dataset label of <Dataset> will be used
  %*       If <Dataset> is unlabelled, then <Dataset> will be used
  %*
  %*     <Heading> is optional
  %*
  %*       Heading controls what appears in Row 1 of the Excel worksheet.
  %*       Heading may be:
  %*         LABEL or L - Column label, name is used if unlabelled column
  %*         NAME or N - Column name
  %*         NONE or 0 or . - No column heading row
  %*       Heading defaults to NAME.
  %*
  %*     <Dataset>
  %*
  %*       A SAS data set or view to be placed in a Excel worksheet.
  %*       <Dataset> may contain any valid dataset options such as where=,
  *%       keep= and drop=.
  %*
  %*     <CatalogEntry>
  %*
  %*       A SOURCE catalog entry. Use either three-level or four-level.
  %*       If catalog entry is captured PROC text output the argument
  %*       should use the ~ prefix for best results.
  %*
  %*  Form 2:
  %*
  %*   <Dataset>|<CatalogEntry>
  %*
  %*     <Dataset>
  %*
  %*       A SAS data set or view to be placed in a Excel worksheet.
  %*       <Dataset> may contain any valid dataset options such as where=,
  *%       keep= and drop=.
  %*       The sheet will be named according to the dataset label.
  %*       If <Dataset> is unlabelled, then <Dataset> will be used
  %*
  %*     <CatalogEntry>
  %*
  %*       A SOURCE catalog entry. Use either three-level or four-level name.
  %*       The worksheet name is the memname.entry part of the resolved four-level name
  %*       and will be ~ formatted.
  %*
  %*-----
  %* Perl executable
  %*
  %*   PERL=<host-path>
  %*
  %*-----
  %* Work location for Perl program and Excel file
  %*
  %*   TEMPDIR=<host-path for temporary files>  default is /tmp deferring to WORK path
  %*
  %*
  %*
  %* Example to create an excel file with three worksheets:
  %*   %sas2xls (
  %*     FILEREF=_WEBOUT
  %*   , SHEET1=SASHELP.CLASS
  %*   , SHEET2=Libraries::SASHELP.VSLIB
  %*   , SHEET3=A list of the A tables:Label:SASHELP.VSTABLE(WHERE=(MEMNAME LIKE 'A%'))
  %*   )
  %*;

  %local NOTES SOURCE SOURCE2 MPRINT;

  %let NOTES   = %sysfunc (getoption(NOTES));
  %let SOURCE  = %sysfunc (getoption(SOURCE));
  %let SOURCE2 = %sysfunc (getoption(SOURCE2));
  %let MPRINT  = %sysfunc (getoption(MPRINT));

  options NONOTES NOSOURCE NOSOURCE2 NOMPRINT;

  %local THIS VERSION;
  %local FILE FILEREF XLS_FILE RC DSID I N L ARG P P1 P2 CTOKEN;
  %local LHS RHS DATASET WSNAME COMMA;
  %local PERL PERL_PGM PERL_RUN PERLPIPE;

  %let THIS = sas2xls;
  %let VERSION = 2002.05.29;

  %*---------------------------------------------------------------------------
  %* Check for XLS destination;

  %if (%nrbquote(&FILEREF) eq ) and (%nrbquote(&FILE) eq ) %then %do;
    %put ERROR: &THIS: No valid FILEREF or FILE was provided;
    %goto EndMacro;
  %end;

  %*---------------------------------------------------------------------------
  %* Check that FILEREF is assigned;

  %if (&FILEREF ne ) %then %do;
    %let FILE= ;
    %if (%sysfunc (fileref(&FILEREF)) > 0) %then
      %put ERROR: &THIS: Fileref &FILEREF is not assigned;
  %end;

  %*---------------------------------------------------------------------------
  %* Set TMP default;

  %if ("&TEMPDIR" eq "") %then
    %let TEMPDIR = /tmp;

  %*---------------------------------------------------------------------------
  %* Prepare to examine each parameter;

  %let I = 1;
  %let N = 0;

  %*---------------------------------------------------------------------------
  %* Examine each value of each sheet<n> parameter;

  %do %while (&I <= 50);

    %if (%superq(SHEET&I) eq) %then %goto NxtSheet;

    %let N = %eval (&N + 1);
    %local DS&N WN&N HD&N;

    %let P1 = %index (&&SHEET&I,:);
    %let P2 = %index (%qsubstr(&&SHEET&I( ),%eval(&P1+1)),:);

    %let CTOKEN =;

    %if &P2 > 0 %then %do;
      %* argument contains at least two :
      %* find out what, if anything is between the :;

      %if &P2 > 1 %then %do;
        %* there is something between the :;

        %let CTOKEN = %qsubstr(&&SHEET&I, %eval(&P1+1), %eval(&P2-1));
        %if %qupcase(&CTOKEN) = L or %qupcase(&CTOKEN) = LABEL %then
          %let CTOKEN = LABEL;
        %else
        %if %qupcase(&CTOKEN) = N or %qupcase(&CTOKEN) = NAME %then
          %let CTOKEN = NAME;
        %else
        %if (&CTOKEN = .) or (&CTOKEN = 0) or (%qupcase(&CTOKEN) = NONE) %then
          %let CTOKEN = NONE;
        %else %do;
          %put WARNING: &THIS: :&CTOKEN: in &&SHEET&I has defaulted to NAME;
          %let CTOKEN = NAME;
        %end;
      %end;
      %else
        %let CTOKEN = NAME;

      %let P2 = %eval (&P1+&P2);
    %end;

    %if (&CTOKEN ne ) %then %do;
      %* sheetname :: dataset|catalog;
      %if &P1 > 1 %then
        %let WN&N = %qsubstr(&&SHEET&I,1,%eval(&P1-1));

      %if &P2 < %length (&&SHEET&I) %then
        %let DS&N = %qsubstr (&&SHEET&I,%eval(&P2+1));
      %let HD&N = &CTOKEN;
    %end;
    %else %do;
      %* dataset|catalog;
      %let WN&N = ;
      %let DS&N = &&SHEET&I;
      %let HD&N = NAME;
    %end;

%NxtSheet:
    %let I = %eval (&I+1);
  %end;

  %*---------------------------------------------------------------------------
  %* Check if any sheets indicated ;

  %if (&N = 0) %then %do;
    %put WARNING: &THIS: No sheets were indicated.;
    %goto EndMacro;
  %end;

  %*---------------------------------------------------------------------------
  %* Write first part of Perl program ;

  %let PERL_PGM = _%substr (%sysfunc (ranuni(0), 9.7), 3);
  %let PERL_RUN = _%substr (%sysfunc (ranuni(0), 9.7), 3);

  %if (%nrbquote(&FILEREF) ne ) %then
    %let XLS_FILE = &TEMPDIR./_%substr (%sysfunc (ranuni(0), 9.7), 3).xls;
  %else
    %let XLS_FILE = &FILE;

  filename &PERL_PGM "&TEMPDIR./&PERL_PGM..pl";

     %let PERLPIPE = &PERL %sysfunc(pathname(&PERL_PGM)) > &XLS_FILE;
%*** %let PERLPIPE = cat %sysfunc(pathname(&PERL_PGM)) | remsh remote-host -l guest-user-for-perl perl -w > &XLS_FILE;

  data _null_;
    file &PERL_PGM recfm=N;

    put   'use strict;'
    '0A'X 'use Spreadsheet::WriteExcel;'
    '0A'X 'my $workbook  = Spreadsheet::WriteExcel->new("-");'
    '0A'X 'my @data = ('
    ;
  run;

  %if &SYSERR ne 0 %then %do;
    %put ERROR: &THIS: An error occurred.;
    %goto EndMacro;
  %end;

  %*---------------------------------------------------------------------------
  %* Write SAS data as Perl array initializations;

  %let COMMA = %str( );
  %do I = 1 %to &N;

%*  %put [&&DS&I] [&&WN&I] [&&HD&I];

    %sas2perl (DATA=&&DS&I, WSNAME=&&WN&I,  HEADING=&&HD&I, PERL_PGM=&PERL_PGM, DATACOMA=COMMA) ;
  %end;

  %*---------------------------------------------------------------------------
  %* Write last part of Perl program ;

  data _null_;
    file &PERL_PGM mod;

    put ');'
     // 'if (scalar @data == 0) { @data=(["Warning",[["No data exported"]]]);}'
     // 'my $courier = $workbook->addformat(font=>"Courier New");'
     // 'my $worksheet;'
     // 'foreach my $sheet ( @data ) {'
      / '  my $sheetName = $sheet->[0];'
      / '  my $sheetData = $sheet->[1];'
      / '  if ( $sheetName =~ /^~/) { '
      / '    $worksheet = $workbook->addworksheet(substr($sheetName,1));'
      / '    $worksheet->write(0,0,[$sheetData],$courier);'
      / '    $worksheet->hide_gridlines(2); '
      / '  }'
      / '  else {'
      / '    $workbook->addworksheet($sheetName)->write(0,0,[$sheetData]);'
      / '  }'
      / '}'
     // "if (__FILE__ ne '-') { unlink __FILE__; } "
    ;
  run;

  %if &SYSERR ne 0 %then %do;
    %put ERROR: &THIS: An error occurred.;
    %goto EndMacro;
  %end;

 /*;

  data _null_;
    infile &PERL_PGM;
    input;
    put _N_ _infile_;
  run;

*/;

  filename &PERL_RUN PIPE "&PERLPIPE";
  data _null_;
    infile &PERL_RUN;
    input;
    put _infile_;
  run;

  %if &SYSERR ne 0 %then %do;
    %put ERROR: &THIS: An error occurred.;
  %end;

  %if (%nrbquote(&FILEREF) ne ) %then %do;
  data _null_;

    %if (%qupcase(&FILEREF) eq _WEBOUT) %then
      file &FILEREF recfm=S;
    %else
      file &FILEREF recfm=N;
    ;

    infile "&XLS_FILE" recfm=n;

    if _n_ = %eval (%qupcase(&FILEREF) eq _WEBOUT) then
      put 'Content-type: application/vnd.ms-excel' '0A0A'x @;

    input c $char1.;
    put c $char1. @@;
  run;
  %end;

%EndMacro:

  %if (&PERL_PGM ne ) %then %do;
    %let RC = %sysfunc (fileref (&PERL_PGM));
    %if &RC <= 0 %then %do;
      %if &RC = 0 %then
        %let RC = %sysfunc (fdelete (&PERL_PGM));
      filename &PERL_PGM;
    %end;
  %end;

  %if (&PERL_RUN ne ) %then %do;
    %let RC = %sysfunc (fileref (&PERL_RUN));
    %if &RC <= 0 %then %do;
      filename &PERL_RUN;
    %end;
  %end;

  %if (&FILEREF ne ) %then %do;
    filename &PERL_RUN "&XLS_FILE";
    %let RC = %sysfunc (fdelete (&PERL_RUN));
    filename &PERL_RUN ;
  %end;

  options &NOTES &SOURCE &SOURCE2 &MPRINT;

%mend sas2xls;


%*****************************************************************************;

%macro perlStr (dsVar);
  &dsVar = tranwrd (&dsVar, "\", "\\");
  &dsVar = tranwrd (&dsVar, "'", "\'");

  if length(&dsVar) > 198 then do;
    &dsVar = substr(&dsVar,1,198);
    if substr (&dsVar,198,1) eq "\" and
       substr (&dsVar,197,1) ne "\"
    then
      &dsVar = substr (&dsVar,1,197);
  end;

  &dsVar = "'" || trim (left (&dsVar)) || "'";
%mend perlStr;


%*****************************************************************************;

%macro sas2perl
(
  PERL_PGM =
, WSNAME =
, DATA =
, HEADING = NAME
, DATACOMA =
);

  %*
  %* Richard A. DeVenezia
  %*  5/17/02
  %*
  %* Output data as an element of a Perl array assignment
  %*
  %* PERL_PGM - fileref to Perl program that will receive data information
  %* WSNAME - name of worksheet to put data in
  %* DATA - name of data set to export
  %* HEADING - name, label or none
  %* DATACOMA - name of macro variable in callers scope, needed for repeated
  %*            invocations of this macro in a loop
  %*
  %* The Perl array is three dimensions and one element looks like
  %*
  %* ["<sheet name>",
  %*  [
  %*   [ r<1>c<1>, ..., r<1>c<m> ]
  %* , [ ... ]
  %* , [ r<n>c<1>, ..., r<n>c<m> ]
  %*  ]
  %* ]
  %*;

  %local NOTES SOURCE SOURCE2 MPRINT;

  %let NOTES   = %sysfunc (getoption(NOTES));
  %let SOURCE  = %sysfunc (getoption(SOURCE));
  %let SOURCE2 = %sysfunc (getoption(SOURCE2));
  %let MPRINT  = %sysfunc (getoption(MPRINT));

  options NONOTES NOSOURCE NOSOURCE2 NOMPRINT;

  %local THIS VERSION CONTENTS ;
  %local RC CATENTRY;

  %let THIS = sas2perl;
  %let VERSION = 2002.05.29;

  %let CONTENTS = _%substr (%sysfunc (ranuni(0), 9.7), 3);
  %let END      = _%substr (%sysfunc (ranuni(0), 9.7), 3);

  %*---------------------------------------------------------------------------
  %* Validate PERL_PGM;

  %if (%superq (PERL_PGM) eq ) %then %do;
    %put ERROR: &THIS: PERL_PGM was either not set or not passed;
    %goto EndMacro;
  %end;
  %else
  %if (%sysfunc (fileref(&PERL_PGM)) > 0) %then %do;
    %put ERROR: &THIS: PERL_PGM Fileref &PERL_PGM is not assigned;
    %goto EndMacro;
  %end;

  %*---------------------------------------------------------------------------
  %* Check if data is coming from a catalog source entry;

  %if (%sysfunc (cexist (&DATA..SOURCE,CATALOG))) %then
    %let CATENTRY = &DATA..SOURCE;
  %else
  %if (%sysfunc (cexist (&DATA))) %then
    %let CATENTRY = &DATA;
  %else
  %if (%sysfunc (exist (&DATA,DATA))) %then
    %let CATENTRY = ;
  %else
  %if (%sysfunc (exist (&DATA,VIEW))) %then
    %let CATENTRY = ;

%put CATENTRY=&CATENTRY;

  %if (&CATENTRY ne ) %then %do;

    %*-------------------------------------------------------------------------
    %* Output the CATALOG SOURCE entry as part of a Perl array initialization;

    %if (%superq(WSNAME) eq ) %then %do;
      %let WSNAME = ~%scan(&CATENTRY,2,.).%scan(&CATENTRY,3,.);
    %end;

    filename &CONTENTS catalog "&CATENTRY";

    data _null_;
      length line $200;

      file &PERL_PGM mod recfm=N;

      line = symget ('WSNAME');

      %perlStr(line);
      put '0A'x "&&&DATACOMA" '0A'x '[' line ', [' ;

      rowComma = ' ';
      do while (not end);
        infile &CONTENTS length=len end=end;
        input line $varying200. len;
        %perlStr(line);

        put '0A'X rowComma '[' line ']';
        rowComma = ',';
      end;

      put '0A'X ']]';

      stop;
    run;

    filename &CONTENTS;

    %let &DATACOMA = , ;

    %goto EndMacro;
  %end;

  %*---------------------------------------------------------------------------
  %* Validate HEADING;

  %let HEADING = %upcase(&HEADING);

  %if %index(|NAME|LABEL|NONE|, |&HEADING|) eq 0 %then %do;
    %put WARNING: &THIS: Heading &HEADING is invalid, it has been set to NAME;
    %let HEADING = NAME;
  %end;

  %if (%superq(WSNAME) eq ) %then %do;
    %let WSNAME = %str( );
  %end;

  %*---------------------------------------------------------------------------
  %* Output the DATA as part of a Perl array initialization;

  %local error;

  %let error = 0;

  data _null_;

    length data $200;

    data = symget ('DATA');
    dsid = open (data);

    if dsid = 0 then do;
      put "WARNING: &THIS: " data "could not be opened.";
      call symput ('error', 1);
      stop;
    end;

    file &PERL_PGM mod recfm=N;

    data = symget ('DATA');
    put '0A0A'x '# ' data;

    data = symget ('WSNAME');
    if data = "" then data = attrc (dsid, 'LABEL');
    if data = "" then data = attrc (dsid, 'MEM');

    %perlStr(data);

    put '0A'x "&&&DATACOMA" '0A'x '[' data ', [' ;

    %*.........................................................................
    %* Header line if requested;

    comma = ' ';
    rowComma = ' ';

    nVar = attrn (dsid, 'NVAR') ;

    put '0A'X rowComma '[';

    do i = 1 to nVar while (%eval(&HEADING ne ));

      %if (&HEADING = LABEL) %then %do;
        data = varlabel (dsid, i); ;
        if data = "" then
          data = varname (dsid, i);
      %end;
      %else
      %if (&HEADING = NAME) %then %do;
        data = varname (dsid, i);
      %end;

      %perlStr (data);

      put comma data ;

      comma = ',';

    end;

    put ']';

    rowComma = comma;  %* comma will be a comma if a header row was output;

    %*.........................................................................
    %* Data portion;

    do while (0 = fetch (dsid));
      put '0A'X rowComma '[';

      comma = ' ';
      do i = 1 to nVar;
        varFmt = varFmt (dsid,i);

        if varType (dsid,i) = 'C' then do;
          data = getVarC (dsid, i);
          if varFmt ne '' then do;
            data = putC (data, varFmt);
            %perlStr (data);
            put comma data;
          end;
          else do;
            %perlStr (data);
            put comma data;
          end;
        end;
        else do;
          dataN = getVarN (dsid, i);
          if varFmt ne '' then do;
            data = putN (dataN, varFmt);
            %perlStr (data);
            put comma data;
          end;
          else do;
            if dataN ne . then
              put comma dataN;
            else
              put comma 'undef';
          end;
        end;
        comma = ',';
      end;

      put ']';
    end;

    rc = close (dsid);

    put '0A'X ']]';

    stop;
  run;

  %if (not &ERROR) %then
    %let &DATACOMA = , ;

%EndMacro:

  options &NOTES &SOURCE &SOURCE2 &MPRINT;

%mend sas2perl;