SAS Guide: Ejecutar código al conectarse

En ocasiones utilizamos repetidas veces las mismas definiciones de librerías que no están predeterminadas por el administrador en el sistema o tenemos que cargar ciertas macros que utilizamos repetidamente en nuestros distintos programas. Este trabajo de repetir estos trozos de código a veces puede resultar pesado e incluso comerse parte de nuestro preciado tiempo.

Una forma para realizar esta tarea de forma automática cada vez que se inicia SAS Guide es utilizando el autoexec. Este es un fichero que contiene un trozo de código que se ejecutará al inicio de la sesión y que es exclusivo de cada usuario, por lo que podemos personalizarlo a nuestro gusto.

Podemos editar este autoexec desde las opciones de SAS Guide. Para ello, primero crearemos el programa que contiene el código que necesitamos ejecutar y lo guardaremos preferiblemente en una ruta de red donde otros usuarios puedan alcanzarlo para que puedan utilizar nuestro código. No es nada recomendable guardarlo en una carpeta personal si se trabaja en un equipo.

Una vez hecho esto, en el menú de SAS Guide vamos al menú Herraminetas > Opciones > Programas SAS y hacemos check en la opción «Procesar el código SAS cuando el servidor esté conectado». Esto ejecutará el siguiente código cuando se realice una conexión al workspace. De esta forma, las librerías o macros que creemos estarán disponibles nada más abrir SAS y podrán ser llamadas desde cualquier programa que ejecutemos nosotros.

Menú de Opciones de SAS Guide

Para editar el código que se ejecutará en ese momento haremos clic en el botón Editar. Aparecerá una ventana de código donde incluiremos la llamada al código que nos interese. En este caso recomiendo incluir un include que llame al programa SAS que ya habíamos generado. Para ello, escribimos:

%include «[ruta]\[nombre_programa.sas]»;

Donde ruta es la ruta completa de la carpeta donde se encuentra el código y nombre_programa.sas el nombre del programa que se ha creado.

Cualquier otro usuario que ejecute un programa que contenga estas librerías o macros que hemos definido aquí; o en caso de planificar un programa, no los tendrán disponibles por defecto, ya que este cambio solo afecta a tu propia sesión. Pero esto se arregla fácilmente incluyendo la misma línea del %include que antes hemos visto al principio de ese programa y todo funcionará correctamente.

Automatización: Ejecución condicionada

El punto que vamos a tratar aquí es cómo hacer que un programa que tenemos planificado no arranque y se ejecute entero en el momento en que el planificador lance en el proceso, sino que espere a alguna otra dependencia que tenga que, por la razón que sea, no está disponible en el planificador, por ejemplo, si estamos utilizando cron o algún planificador de tareas básico que no pueda gestionar dependencias, puede ser una buena idea comprobar antes que todas las tablas que necesitamos están actualizadas.

Para ello vamos a necesitar una macro (llamada ejecucion en el ejemplo de código), que contiene el bucle y el código a ejecutar. Lo primero que hacemos es evaluar la condición de ejecución (antes del bucle while), si la condición no es cierta, se ejecutará el contenido del bucle y si es correcta ejecutará el código.

En un paso data dentro del bucle calculamos la hora actual y la hora límite que queremos dar al proceso (en este caso, la hora límite son las 22:00PM). Con esta hora límite, evitamos que el proceso se quede embuclado hasta el infinito en caso de que la condición que hemos establecido para num_tablas_actualizadas no se de nunca. Las funciones de fecha que se utilizan las hemos explicado en SAS: Operaciones con fechas y los formatos de fecha en Formatos de fecha en SAS.

La función sleep() debe ir dentro de un paso data y obliga a esperar al proceso por un periodo de tiempo. Puede tener uno o dos parámetros. Si solo le damos uno ese serán el número de unidades a esperar con la particularidad de que las unidades en un sistema Windows serán segundos y en otro basado en Linux, serán milisegundos. La forma de hacer que el código sea portable entre sistemas distintos es darle dos parámetros de forma que el primero siguen siendo el número de unidades y el segundo indicará cual es esa unidad. Por ejemplo, 1 significará segundos y 0.001 milisegundos.

Como último paso del bucle while se vuelve a evaluar la condición de ejecución (en el proc sql) y se comienza una nueva iteración en la que se comprueba esa condición y también se comprueba que no se haya llegado a la hora límite. Si se cumplen las condiciones el flujo se saltará el bucle y comenzará con la ejecución del resto del código.

%macro ejecucion();
proc sql noprint;
select count(*) into :num_tablas_actualizadas
from XXXXXX where YYYYYY
;quit;
%put "WARNING: Hay &num_tablas_actualizadas tablas actualizadas a la hora de inicio del proceso.";

%do %while (&num_tablas_actualizadas ne 5);
data _null_;
call symput('a',put(datetime(),datetime20.));
call symput('b',put(dhms(date(),22,00,0),datetime20.));
x=sleep(15,60);
run;
%put "WARNING: Hay &num_tablas_actualizadas tablas actualizadas a las &a";

proc sql noprint;
select count(*) into :num_tablas_actualizadas
from XXXXXX where YYYYYY
;quit;

data _null_;
if &a > &b then do;
%put "WARNING: Ejecución terminada al superarse la hora límite.";
stop;
end;
run;
%end;

/* inicio del proceso */;
%mend;
%ejecucion;

Ejecutar un programa SAS pasando parámetros

La entrada de hoy es la solución a un problema muy fácil: se trata de planificar un programa sas pasándole algún parámetros desde línea de comandos, en este caso, la solución aplica a Unix/Linux.

Para ello, debemos crear un fichero de script (.sh) que contenga la llamada y los parámetros a enviar:

sh [ruta_SAS]/Lev1/SASApp/sas.sh -set fecha 20201111 -set hora 17:28 -sysin [ruta_programa]/programa.sas -log [ruta_log]/fichero.log

La llamada a sas.sh ejecuta sas.exe con las opciones que estén configuradas para el usuario. El parámetro set contiene una dupla clave valor con el nombre de la variable a enviar al programa en ejecución y su valor. Se pueden concatenar varios parámetros set. En el ejemplo se pasa la fecha y la hora. Finalmente, los parámetros sysin y log indican la ruta y fichero del programa sas y del log que se debe generar, respectivamente.

Una vez hecho esto, solo queda recoger en el programa los valores que hemos enviado. Esto podemos hacerlo de la siguiente forma:

%let fecha=%sysget(fecha);
%let hora=%sysget(hora);

Con ello, podemos utilizar los parámetros que hemos enviado como simples macrovariables: &fecha y &hora.

Truco SAS: Ejecución condicionada con variables de sistema

En ocasiones me ha pasado que estoy trabajando en versionar o modificar un código que ya está planificado y ejecutándose actualmente. Por lo que tengo una versión del programa ejecutando en el servidor y otra versión que desarrollo en el SAS Enterprise Guide. Lo que me pasa es que hay partes que no quiero ejecutar en el Guide, bien porque tardan, bien porque no las necesito o porque solo se pueden ejecutar en el servidor.

Para no tener que estar ejecutando un trozo de código sí, otro no, hay un pequeño truco para dejarlo todo listo para que la misma versión de código pueda correr en ambos entornos pero haciendo en cada uno lo que necesito. Este truco recurre a las variables de sistema de SAS.

SYSUSERID es una variable de sistema de SAS que informa del usuario que está ejecutando. En una instalación típica de SAS, el usuario que utiliza SAS Guide cuando trabajamos con él es «sassrv». Cuando planificamos un proceso en el servidor se ejecuta con «sasbatch». Esta es la diferencia que explotaremos ene l siguiente ejemplo para lograr ejecutar una parte del código solamente en el servidor:

options mlogic;
%macro en_servidor;
    %if "&SYSUSERID"="sasbatch" %then %do;
        data a;
            a=1;
        run;
    %end;
%mend;

%en_servidor;

Si ejecutamos este código en el Guide no sucederá nada: no se creará la tabla A. He añadido la opcion options mlogic; que incluye en el log información sobre las decisiones lógicas que toma (el resultado de los %if). Si revisamos el log podremos ver que indica que el usuario NO ES sasbatch.

MLOGIC(EN_SERVIDOR):  Empezando la ejecución.
MLOGIC(EN_SERVIDOR):  la condición %IF "&SYSUSERID"="sasbacht" es FALSE
MLOGIC(EN_SERVIDOR):  finalizando la ejecución.

Si creamos otra macro en la que modificamos el código utilizando el usuario de SAS Guide (sassrv) veremos que sí se ejecuta:

options mlogic;
%macro en_guide;
    %if "&SYSUSERID"="sassrv" %then %do;
        data a;
            a=1;
        run;
    %end;
%mend;

%en_guide;

Ahora el resultado del %if sí es verdadero, y el paso data se ejecutará.

MLOGIC(EN_GUIDE):  Empezando la ejecución.
MLOGIC(EN_GUIDE): la condición %IF "&SYSUSERID"="sassrv" es VERDADERA
MPRINT(EN_GUIDE):   data a;
MPRINT(EN_GUIDE):   a=1;
MPRINT(EN_GUIDE):   run;

NOTE: The data set WORK.A has 1 observations and 1 variables.
NOTE: Sentencia DATA used (Total process time):
      real time           0.00 seconds
      cpu time            0.00 seconds

MLOGIC(EN_GUIDE):  finalizando la ejecución.

Punteros de datos en SAS

Bajo mi opinión los punteros de datos son una de las herramientas más potentes que tenemos. Me gusta mucho usarlos y seguro los veréis en en muchas de las macros que voy creando. Siempre llega un punto en que es más fácil utilizar un puntero para recorrer una tabla mientras se está en un paso data con otra tabla. Esta forma de trabajo está recomendada para evitar tener que hacer productos cartesianos cuando las tablas son muy grandes, ya que ahora muchísimo espacio y también recursos de máquina.

Un puntero de datos se declara con una sentencia open que abre la tabla indicada crea el stream de datos; devuelve un código de respuesta en función del resultado de la operación.

%let dsid = %sysfunc(open(&tabla));

Usualmente los primero que hacemos es valorar el tamaño de la tabla, probablemente para poder recorrerla con bucles. Para ellos tenemos aquí dos atributos útiles: nlobs y nvars; para calcular el número de observaciones y de columnas de una tabla, respectibamente. Cuidado aquí con otro atributo parecido: nobs, que devuelve el número de registros físicos, no los lógicos. La diferencia estriba en que los registros marcados para borrar en esa tabla no serán contabilizados por nlobs

%let nobs =%sysfunc(attrn(&dsid,nlobs));
%let nvars=%sysfunc(attrn(&dsid,nvars));

Antes de recorrer la tabla tenemos dos atributos útiles: fetch, que lee el siguiente registro no eliminado de la tabla y fetchobs que sitúa el cursor en el número de registro indicado:

%let rc=%sysfunc(fetch(&dsid));
%let rc=%sysfunc(fetchobs(&dsid,1));

Ambas rutinas devuelven 0 en caso de éxito, -1 en caso de haber llegado al final del fichero y cualquier otro valor en caso de algún tipo de error. El error se puede recogar con %sysfunc(sysmsg()).

La consulta la vector de datos la haremos del siguiente modo y dependiendo de si la variables es una cadena o un número, por supuesto las fechas son números. Primero utilizamos varnum para identificar el número de variable dentro de la tabla y luego getvarc o getvarn para obtener su valor:

%let variable_num = %sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,variable_num))));
%let variable_char = %sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,variable_char))));

Finalmente es importante cerrar el puntero después de usarlo con el comando close>/code>:

%let rc = %sysfunc(close(&dsid));

Con todo ello tenemos las herramientas para poder utilizar vectores de datos con SAS.

Automatización del borrado de ficheros

La automatización del borrado de ficheros es una tarea necesaria cuando, por ejemplo, estamos generando en un proceso planificado ficheros fechados periódicamente. Hacer exhaustivamente esta tarea es complicado y no siempre la realizamos por esa razón (yo al menos). Esta macro da solución a esa necesidad, os animo a usarla.

Lo primero que hacemos es crear un stream de datos, df, que recoge el resultado del comando ls. El parámetro F marca los directorios con una barra «/» al final, de forma que podemos eliminarlos de la salida del ls con un grep -v.

Usando df como entrada para nuestro paso data todo lo que queda es procesar las información que hemos generado con un scan, que divide la cadena por los espacios. Finalmente solo nos queda montar la fecha de creación del fichero teniendo en cuenta lo siguiente: Linux devuelve la fecha en dos formatos distintos dependiendo de si hace menos de 6 meses que se ha generado el fichero o si hace más. Este es un ejemplo:

Formatos de fecha en un ls

Finalmente, filtramos los registros que contienen los nombres de los ficheros que queremos eliminar: ficheros anteriores a 2020 y que no sean tablas SAS. Un call system ejemcutará un comando rm con cada registro resultante. Esta es la macro:

%macro borrado(ruta=,fecha_limite=);
    x "cd &ruta";
    filename df pipe "ls -lahF &ruta | grep -v /";

    data _null_;
        infile df;
        input todo $300.;
        format fecha date9. fichero $100.;
        dia = scan(todo,7," ");
        if index(scan(todo,8," "),":") then anyo=year(date());
           else anyo=input(put(scan(todo,8," "),$5.),8.);
        if compress(scan(todo,6," "))='Jan' then mes=1;
        else if compress(scan(todo,6," "))='Feb' then mes=2;
        else if compress(scan(todo,6," "))='Mar' then mes=3;
        else if compress(scan(todo,6," "))='Apr' then mes=4;
        else if compress(scan(todo,6," "))='May' then mes=5;
        else if compress(scan(todo,6," "))='Jun' then mes=6;
        else if compress(scan(todo,6," "))='Jul' then mes=7;
        else if compress(scan(todo,6," "))='Aug' then mes=8;
        else if compress(scan(todo,6," "))='Sep' then mes=9;
        else if compress(scan(todo,6," "))='Oct' then mes=10;
        else if compress(scan(todo,6," "))='Nov' then mes=11;
        else if compress(scan(todo,6," "))='Dec' then mes=12;
        fecha = mdy(mes,dia,anyo);
        if fecha > date() then fecha=intnx('year',fecha,-1,'S');
        fichero = compress(scan(todo,9," "),'*');
        if fecha < &fecha_limite and not index(fichero,'.sas7bdat');

        call system('rm '||fichero);
    run;
%mend;

%borrado(ruta=[rute],fecha_limite='1jan2020'd);

Automatización del borrado de tablas

En ocasiones queremos automatizar un proceso que genera tablas diaria o semanalmente y rápidamente se acumulan un montón de datos en nuestra librería de salida. Necesitamos gestionar esto, pero no podemos estar pendientes de ello. Utilizaremos entonces una macro que nos permite desatender el borrado con unas especificaciones y nos libere de ese trabajo.

Listado de tablas en la librería SASHELP

La macro está mínimamente parametrizada y utiliza el diccionario de tablas de SAS y el proc datasets. Tal como está definida realiza un borrado de la WORK de tablas de más de 3 meses de antigüedad. Evidentemente, no está pensada para usarla contra la WORK.

%macro borrado_historico;
    %let libreria = WORK;
    %let tablas = ;
    data _null_;
        call symput('fecha_limite',put(intnx('month',date(),-3,'S'),date9.));
    run;

    proc sql noprint; 
        select memname into :tablas separated by ' '
        from DICTIONARY.TABLES 
        where libname= "&libreria" and
              memname ne '_PRODSAVAIL' and
              datepart(crdate) < "&fecha_limite"d;
    quit;

    proc datasets lib=&libreria noprint;
        delete _PRODSAVAIL &tablas;
    run;
%mend;
%borrado_historico;

Primero realiza una consulta al diccionario de tablas (DICTIONARY.TABLES) por la fecha de creación de la tabla crdate (también se puede hacer por la fecha de modificación de la tabla: modate) y listo las tablas que serán borradas separadas por espacios para que puedan ser utilizadas directamente por el próximo paso. Añadir al filtro una condición substr(memname,1,9)='CAMPANAS_' nos permitirá borrar solo las tablas de determinado tipo, por supuesto.

En el proc datasets definimos la librería que queremos atacar y le pasamos al statement delete el listado de variables que previamente hemos calculado. Aquí he usado un pequeño truco para evitar que se produzca un error en el caso de que el listado de tablas esté vacío: y es que en el proc sql había excluido la tabla _PRODSAVAIL que añado ahora aquí para que siempre tenga algo que borrar.

¿Qué es la tabla _PRODSAVAIL? Pues contiene un listado de los módulos licenciados en SAS Enterprise Guide para que la herramienta lo pueda consultar rápidamente. Se genera automáticamente con cada sesión que se genera y no produce ningún efecto adverso en el Guide su borrado.

Automatización de procesos con SAS

SAS Enterprise Guide tiene un planificador de tareas que no está mal pero también se puede planificar tareas en el propio servidor. La solución que os ofrezco está basada en un servidor con un sistema operativo Linux.

Una automatización tiene 3 partes: el código SAS a ejecutar, un script que llama al programa y un planificador de tareas que llama al anterior.

Programa SAS (fichero .sas)

Es necesario eliminar de él cualquier rastro de parámetro o fecha variable que hallamos estado empleando cuando lo desarrollamos y debemos calcular esos valores en función de la información que tendremos en la máquina en el momento de la ejecución: como por ejemplo la fecha de sistema. Si queremos hacer un cierre de mes lo más sensato será calcular la fecha de cierre como:

fecha_ejecución = intnx('month',date(),-1,'E');

 
Además recomiendo incluir al principio del programa la siguiente línea que imprime en el log del proceso el número de tarea que se está ejecutando. De esa manera podremos matar un proceso colgado con un comando kill.

%put &sysjobid;

 
TIP: Al hacer una primera automatización es recomendable verificar cual es la fecha del sistema en el servidor SAS, porque podría ser distinta a la actual y eso puede generar problemas.

Script (fichero .sh)

Este pequeño programa es el encargado de identificar la ruta donde se encuentra [sas.exe] y en este caso utiliza además otro script [sas.sh] para que se ejecuten los parámetros de ejecución de sas.exe. También necesita la ruta y el nombre del programa .sas a ejecutar y la ruta y nombre del fichero que se creará con el log de la ejecución. Es importante tener en cuenta esto porque sino nunca sabremos por qué fallan nuestros procesos.

#!/bin/sh
f_job=[ruta_programa]/[nombre_programa].sas
f_log_sas=[ruta_log]/`date +%Y%m%d%H%M`_"$1"[nombre_log].log
sh [ruta_base_sas]/sas.sh -sysin ${f_job} -log ${f_log_sas}

 
Ambos ficheros el .sas y el .sh deben ser copiados con FileZilla u otra herramienta similar a las rutas que hemos visto.

Planificador de tareas

Utilizo el planificador de tareas de Linuz, CRON. Cron tiene un demonio corriendo en el sistema operativo que lanza las tareas que se han indicado en un fichero de texto: crontab. Cada usuario del sistema tiene un fichero crontab para sus propias tareas.

En nuestro caso, para que SAS tenga permisos sobre las ficheros y tablas que necesitamos utilizar en nuestro proceso es necesario utilizar un usuario de Linux que sea también usuario en SAS. Existen varios usuarios de esta naturaleza en una instalación de SAS: sasbatch, sasdemo, etc. El adecuado para planificar un proceso es sasbatch.

Para editar el fichero crontab debemos logarnos en la máquina y acceder al shell para introducir este comando:

crontab

El programa se abre con el visor vi. Para editarlo pulsamos [INSERT]. A continuación introduciremos una línea por programa sas a planificar. Finalmente introducimos las combinaciones de teclas: [END]:q! para salir sin guardar y [ESC]:wq para salvar y salir. No voy a entrar definir aquí como se planifica una tarea pero os dejo un enlace a la Wikipedia donde se explica muy bien:

En un próximo post comentaré errores típicos que se pueden producen al planificar.