Mostrando las entradas con la etiqueta bash. Mostrar todas las entradas
Mostrando las entradas con la etiqueta bash. Mostrar todas las entradas

miércoles, 2 de mayo de 2012

Descargar todas las imágenes de un blog de Tumblr



Advertencia:
El contenido de este artículo está presentado solo con fines didácticos, como demostración de como utilizar herramientas GNU y expresiones regulares para procesar una gran cantidad de información de forma automática.  
La propiedad intelectual de las imágenes en el sitio de Tumblr es de sus respectivos dueños. 


Hace unos días se me ocurrió intentar bajar todas las imágenes de un blog de Tumblr, pero hacerlo a mano tomaría demasiado tiempo, por lo que empecé a analizar la forma de automatizar la tarea.


Ya me había fijado en que todas las imágenes que estaba bajando estaban alojadas en dominios que empiezan con dos números seguidos de .media.tumblr.com y los nombres empiezan con tumblr_. Veamos un ejemplo: la imágen del post http://ilovephotographyclub.tumblr.com/post/22189996450/via-on-my-way-to-heaven-by-farhadvm-on 
es http://27.media.tumblr.com/tumblr_m3chaqtcbC1roly7jo1_1280.jpg.


Para este artículo voy a utilizar herramientas GNU, que pueden utilizarse tanto bajo las variantes *nix (Linux, FreeBSD, MacOS, etc.) como bajo Windows si instalamos cygwin (lo cual recomiendo encarecidamente). 


Podemos utilizar wget para recuperar el contenido de la web y sed para parsear el código html de la página para recuperar las direcciones de la imágenes. Con la siguiente expresión regular '/="http:\/\/.*media\.tumblr\.com\/tumblr_.*"/ s/.*"(http:\/\/.*\.[a-zA-Z]{3,4})".*/\1/p' coincidimos las URL que nos interesan.  


Ejecutando lo siguiente:



$ wget -qO- http://ilovephotographyclub.tumblr.com/post/22189996450/via-on-my-way-to-heaven-by-farhadvm-on | sed -rn -e '/="http:\/\/.*media\.tumblr\.com\/tumblr_.*"/ s/.*"(http:\/\/.*\.[a-zA-Z]{3,4})".*/\1/p'
http://26.media.tumblr.com/tumblr_m3chaqtcbC1roly7jo1_250.jpg
http://27.media.tumblr.com/tumblr_m3chaqtcbC1roly7jo1_1280.jpg


y terminamos con dos lineas que son los enlaces a las dos versiones de la imagen, una en baja resolución y la otra en una mayor resolución.

Ahora simplemente podemos bajar las imágenes con:

$ wget http://26.media.tumblr.com/tumblr_m3chaqtcbC1roly7jo1_250.jpg
$ wget http://27.media.tumblr.com/tumblr_m3chaqtcbC1roly7jo1_1280.jpg

Hasta acá solo probamos nuestra teoría de obtener los enlaces de los posts, ahora veamos como podemos aplicar esto a todos los posts del blog.

Vamos a trabajar con la página principal del blog para parsearla y recuperar de ahí los enlaces a cada post en particular. Personalmente no encontré una forma de hacer esto en pocos pasos, especialmente después de probarlo con varios blogs. La siguiente linea de comando nos retorna la lista de posts que solo pertenecen al blog que estamos procesando (muchas veces hay referencias a otros blogs de donde proviene la imágen).

$ wget -qO- http://ilovephotographyclub.tumblr.com | sed -rn -e "/tumblr\.com\/post/ s/.*(\"http:\/\/.*\.tumblr\.com\/post.*\").*/\1/p" | sed -rn -e 's/"([^"|^#]*)(["#].*)/\1/p' | sort | uniq


El comando recupera la página, filtra por los enlaces a posts y luego elimina el texto redundante que pasó por el primer filtro, también se aprovecha para eliminar referencias a la misma página (#), luego se ordena con sort para que uniq nos devuelva una lista única.

Ahora sería interesante aplicar esto a todos los posts del blog, si pudiéramos encontrar la forma de acceder a algún tipo de lista de los mismos. La página archive del blog nos permite acceder al historial el blog, pero muestra solo los últimos posts en orden descendente, al ir bajando -mediante javascript- va agregando dinámicamente el resto de los posts mas antiguos que no aparecieron en la página al cargarse. Si intentamos recuperar esta página con wget tenemos solo la página inicial y no todo el archivo por lo que no es práctico para nuestros intereses.

Otra forma de acceder al archivo de Tumblr es a través de páginas. Se pueden acceder ellas a través de la subcarpeta page seguida del número de página a la que queremos acceder. Ej: http://blog.tumblr.com/page/3 para acceder a la página 3.

Con esto podemos recorrer todas las páginas con un contador y un bucle, hasta que lleguemos al final de las páginas. Si solicitamos una página posterior a la última que tenga contenido, el sitio nos devuelve una página sin enlaces a post alguno, con formato pero vacía, podría decirse.

Ya no podemos probar este concepto directamente desde la linea de comandos, tendremos que utilizar un script.

#!/bin/bash

PAGE_NUM=1
SALIR=0
BASE_URL="http://$1.tumblr.com"

while [ $SALIR -eq 0 ]; do
  SITE="$BASE_URL/page/$PAGE_NUM"
  echo "procesando la página $PAGE_NUM [$SITE]"
  POST_LIST=`wget -qO- $SITE | sed -rn -e "/$1\.tumblr\.com\/post/ s/.*(\"http:\/\/.*\.tumblr\.com\/post.*\").*/\1/p" | sed -rn -e 's/"([^"|^#]*)(["#].*)/\1/p' | sort | uniq`
  if [ -z "$POST_LIST" ]; then
    SALIR=1
  else
    for POST in $POST_LIST; do
      echo $POST
    done
    let PAGE_NUM=$PAGE_NUM+1
  fi
done

Este script entra en un loop en el que incrementaremos nuestro contador de páginas, recuperaremos los enlaces a posts de cada página, si no podemos recuperar ningún enlace mas significa que llegamos al final de las páginas, entonces salimos del loop. La única acción del script es recorrer la lista y mostrar los enlaces. Debemos de pasar el nombre del blog como parámetro. Ej: 

$ sh dwn_tumblr_test.sh ilovephotographyclub

Teniendo todo esto, es hora de programar un script que implemente todos los conceptos que probamos a lo largo del artículo.

#!/bin/bash

BLOG=$1
LOG="$1.log"
URL="http://$1.tumblr.com"
ARCHIVE="$URL/archive"
DUMP_DIR=$1


echo -e "Iniciando recuperacion del blog $1\n" > $LOG
echo "URL: $URL" >> $LOG

echo -e "Iniciando recuperacion del blog $1\n" 
echo "URL: $URL"

# creamos la carpeta de salida
if [ -e $1 ] && [ -d $1 ]; then
  echo "usando directorio $PWD/$1" >> $LOG
  echo "usando directorio $PWD/$1"
else
  echo "directorio $PWD/$1 no existe, creando." >> $LOG
  echo "directorio $PWD/$1 no existe, creando." 
  mkdir $1 >> $LOG
fi

echo "" >> $LOG

PAGE_NUM=1 # el número de página que vamos a procesar
SALIR=0    # el loop iteractuará mientras esta variable sea 0
while [ $SALIR -eq 0 ]; do
  PAGE_URL="$URL/page/$PAGE_NUM"
  echo "procesando la página $PAGE_NUM [$PAGE_URL]"
  echo "procesando la página $PAGE_NUM [$PAGE_URL]" >> $LOG
  
  POST_LIST=`wget -qO- $PAGE_URL | sed -rn -e "/$1\.tumblr\.com\/post/ s/.*(\"http:\/\/.*\.tumblr\.com\/post.*\").*/\1/p" | sed -rn -e 's/"([^"|^#]*)(["#].*)/\1/p' | sort | uniq`
  if [ -z "$POST_LIST" ]; then
    SALIR=1
  else # if [ ! -z "$POST_LIST" ] ...
    for POST in $POST_LIST; do
   # recuperamos una lista de los enlaces de las imágenes del post. normalmente hay varias versiones
   # de la imágen posteada en varias resoluciones 
   IMG_URL_LIST=`wget -qO- $POST | sed -rn -e '/="http:\/\/.*media\.tumblr\.com\/tumblr_.*"/ s/.*("http:\/\/.*media\.tumblr\.com\/tumblr_.*").*/\1/p' | sed -rn -e 's/"([^"|^#]*)(["#].*)/\1/p' | sort | uniq`
  
   # recorremos la lista de imágenes
   for IMG_URL in $IMG_URL_LIST; do
  echo "      url: $IMG_URL"
    
  # recuperamos el nombre del archivo 
  FILE_NAME=`basename $IMG_URL`
  echo "      filename: $FILE_NAME"
    
  # para ahorrar tiempo solo bajamos el archivo si no existe en el directorio de salida 
  if [ -e "$DUMP_DIR/$FILE_NAME" ]; then
    echo "url: $IMG_URL #filename: $FILE_NAME  post:$POST" >> $LOG
    echo "# ya existe"
  else
    echo ">> bajando"
    echo "url: $IMG_URL >filename: $FILE_NAME  post:$POST" >> $LOG    
    wget -qO "$DUMP_DIR/$FILE_NAME" $IMG_URL >> $LOG
  fi
   done # for IMG_URL in $IMG_URL_LIST ...
    done # for POST in $POST_LIST ...
    let PAGE_NUM=$PAGE_NUM+1
  fi # if [ ! -z "$POST_LIST" ] ...
done # while [ $SALIR -eq 0 ] ...

Guardamos el script en un archivo y lo ejecutamos, siempre pasando el nombre del blog como parámetro:


$ sh dwn_tumblr.sh ilovephotographyclub
y al terminar tendremos un directorio con el nombre del blog con las imágenes y un archivo también con el mismo nombre pero con extensión .log con el detalle de todo lo descargado.


Todo el ejemplo aquí expuesto fue creado y probado con CygWin bajo Windows 7 x64.


Actualización 03/05/2012: En el último script, en la linea 42 se agregó "| sort | uniq"  al código a fin de eliminar duplicados:


IMG_URL_LIST=`wget -qO- $POST | sed -rn -e '/="http:\/\/.*media\.tumblr\.com\/tumblr_.*"/ s/.*("http:\/\/.*").*/\1/p' | sed -rn -e 's/"([^"|^#]*)(["#].*)/\1/p'`

por

IMG_URL_LIST=`wget -qO- $POST | sed -rn -e '/="http:\/\/.*media\.tumblr\.com\/tumblr_.*"/ s/.*("http:\/\/.*").*/\1/p' | sed -rn -e 's/"([^"|^#]*)(["#].*)/\1/p' | sort | uniq`



En las lineas 54 y 58 se agregó "post: $POST" al texto del echo, a fin de registrar de cual entrada se recuperó la imágen.

jueves, 19 de abril de 2012

Autómatas con Genexus Ev1 en Linux y Windows, y II

En el capítulo anterior creamos un programa de ejemplo con unos requerimientos específicos con la intención de ejecutarlo periódicamente de forma automática.

Ahora que tenemos nuestro programa funcionando, vamos a ponerlo a funcionar bajo linux.

La idea es crear un script del shell para llamar a nuestro programa, hacer algunas verificaciones y enviar el reporte resultante por correo.


Verificaciones previas

Antes que nada debemos asegurarnos que nuestra máquina linux puede enviar mensajes de correo y tenemos instalada alguna máquina virtual java.

Correo

Para este proyecto utilizaremos Sendmail, que se instala con casi todas las distribución de linux mas conocidas. Cómo configurar sendmail escapa del objetivo de este artículo, hay un montón de artículos al respecto en la red.


Podemos enviar un mensaje de prueba con el siguiente comando:

echo -e "Subject: ping \n\nthis goes on the body"  | sendmail -f micuenta@dominio.com otracuenta@gmail.com

Verificamos si tenemos java instalado

Para distribuciones basadas en Debian utilizamos el siguiente comando:

dpkg --get-selections | grep openjdk





Para distribuciones que utilizan RPM utilizamos esto:

rpm -qa | grep openjdk


Copiar archivos al servidor

A los efectos de nuestro proyecto crearemos una carpeta chequeodesatendido en /opt, aunque no es obligatorio y podría ir en otro lugar, por ejemplo en /usr/local o /usr/lib.


Ahora debemos copiar nuestro programa a la caja linux, personalmente utilizo WinSCP. En realidad no necesitamos copiar todos los archivos que nos creó el Deployment Wizard, solo necesitamos las carperas Shared y chequeodesatendido.




Pruebas preliminares

Ya tenemos todo lo necesario para empezar a probar nuestro programa. Creamos un archivo llamado test.sh y le agregamos el siguiente código:



#!/bin/bash
# get current date with file name friendly format
FECHA=`date +"%Y-%m-%d_%H-%M"`

# output file
ARCHIVO_SALIDA=cr_$FECHA

# output folder
DIRECTORIO_SALIDA="$PWD/reportes"

GXCLASSPATH="Shared/.:Shared/gxclassp.jar:Shared/iText.jar:chequeodesatendido/chequeodesatendido_GXWS.jar"


HOY=`date +"%d/%m/%y"`
java -cp $GXCLASSPATH achequeodesatendido "$HOY" "$HOY" "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA" $> "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log"

En este script la variable FECHA captura la fecha y la hora del sistema, a diferencia del script que utilizamos bajo windows que solo capturaba la fecha.


Atender que la lista GXCLASSPATH bajo linux debe estar separada por ":" mientras que bajo windows va separada por ";"


La variable HOY es nueva en este script y es simplemente la fecha del sistema en el órden y formato que espera nuestro programa.


Ejecutamos la prueba:


sh test.sh




Como en el capítulo anterior, deberíamos tener un PDF en la carpeta reportes. 


El script definitivo

Creamos un script llamado run.sh y le agregamos el siguiente código:


#!/bin/bash

#cambiamos al directorio base
cd /opt/chequeodesatendido

FECHA=`date +"%Y-%m-%d_%H-%M"`
ARCHIVO_SALIDA="cr_$FECHA"
DIRECTORIO_SALIDA="$PWD/reportes"
GXCLASSPATH="Shared/.:Shared/gxclassp.jar:Shared/iText.jar:chequeodesatendido/chequeodesatendido_GXWS.jar"

# a quien notificar
LISTA_NOTIFICACION="destino1@dominio.com"

# si tenemos mas de un destinatario, separar cada uno con un espacio
#LISTA_NOTIFICACION="destino1@dominio.com destino2@dominio.com destino3@dominio.com"
REMITENTE='verificador@dominio.com'
SEPARADOR="$$-$$-$$-"

# tamaño de la carpeta
TAMANHO_ACTUAL=`du -h $DIRECTORIO_SALIDA`

enviar_mail() {
    local DIRECCION_CORREO
    local REPORTE
    local ERRORES
    local FECHA_LINDA

    # codificamos el reporte en base64 y lo cargamos en una variable. esto va
    # a ir como un atado al mensaje.
    REPORTE=`base64 $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.pdf`

    # recuperamos las lineas del log en otra variable, estas no van a ir como
    # atado sino en el cuerpo del texto.
    ERRORES=`cat "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log"`

    # podemos filtrar las lineas, por ejemplo para ignorar advertencias o depuración.
    #ERRORES=`cat $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log | grep "Errores:"`

    # fecha en formato mas amigable
    FECHA_LINDA=`date +"%d/%m/%Y %H:%M"`

    # enviamos un mensaje por cada dirección de la lista de notificación.
    # también se podría enviar un solo mensaje con las direcciones en CC.
    for DIRECCION_CORREO in $DIRECCION_NOTIFICACION; do
        /usr/sbin/sendmail -f $REMITENTE -t <<EOF
MIME-Version: 1.0
To: $DIRECCION_CORREO
From: $REMINTENTE
Subject: Verificación de consistencia - $FECHA_LINDA - $ERRORES
Content-Type: multipart/mixed; boundary="$SEPARADOR"

--$SEPARADOR
Content-Type: text/plain; charset=UTF8; format=flowed
content-transfer-encoding: 8bit

$ERRORES

Reporte generado al $FECHA_LINDA
Archivo adjunto: $ARCHIVO_SALIDA.pdf

Tamaño actual del directorio: $ACTUAL_SIZE

--$SEPARADOR
Content-Type: application/pdf; name="$ARCHIVO_SALIDA.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="$ARCHIVO_SALIDA.pdf"

$REPORTE

--$SEPARADOR
Content-Type: text/plain; name="$ARCHIVO_SALIDA.log"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="$ARCHIVO_SALIDA.log"

`cat $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log`

EOF
    done

}

# generar el reporte
HOY=`date +"%d/%m/%y"`
java -cp $GXCLASSPATH achequeodesatendido "$HOY" "$HOY" "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA" $> "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log"


# si se generó el archivo de salida, lo enviamos.
if [ -f $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.pdf ]; then
    echo "Existe $ARCHIVO_SALIDA"
    enviar_mail
fi


Analicemos el script. 


#!/bin/bash

# cambiamos al directorio base
cd /opt/chequeodesatendido

FECHA=`date +"%Y-%m-%d_%H-%M"`
ARCHIVO_SALIDA="cr_$FECHA"
DIRECTORIO_SALIDA="$PWD/reportes"
GXCLASSPATH="Shared/.:Shared/gxclassp.jar:Shared/iText.jar:chequeodesatendido/chequeodesatendido_GXWS.jar"

# a quien notificar
LISTA_NOTIFICACION="destino1@dominio.com"

# si tenemos mas de un destinatario, separar cada uno con un espacio
#LISTA_NOTIFICACION="destino1@dominio.com destino2@dominio.com destino3@dominio.com"
REMITENTE='verificador@dominio.com'
SEPARADOR="$$-$$-$$-"

# tamaño de la carpeta
TAMANHO_ACTUAL=`du -h $DIRECTORIO_SALIDA`

Hasta acá solo son definiciones de variables que utilizaremos mas tarde. SEPARADOR es un texto arbitrario que definiremos para separar las secciones del mensaje, explicaré eso mas adelante. Como siempre a modo de ejemplo en la variable TAMANHO_ACTUAL recuperamos el espacio en disco ocupado por los reportes y la incluiremos en el cuerpo del mensaje.

A continuación tenemos la función que construye y envía el mensaje de correo. 



enviar_mail() {
    local DIRECCION_CORREO
    local REPORTE
    local ERRORES
    local FECHA_LINDA

    # codificamos el reporte en base64 y lo cargamos en una variable. esto va
    # a ir como un atado al mensaje.
    REPORTE=`base64 $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.pdf`

    # recuperamos las lineas del log en otra variable, estas no van a ir como
    # atado sino en el cuerpo del texto.
    ERRORES=`cat "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log"`

    # podemos filtrar las lineas, por ejemplo para ignorar advertencias o depuración.
    #ERRORES=`cat $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log | grep "Errores:"`

    # fecha en formato mas amigable
    FECHA_LINDA=`date +"%d/%m/%Y %H:%M"`

    # enviamos un mensaje por cada dirección de la lista de notificación.
    # también se podría enviar un solo mensaje con las direcciones en CC.
    for DIRECCION_CORREO in $DIRECCION_NOTIFICACION; do
        /usr/sbin/sendmail -f $REMITENTE -t <<EOF
MIME-Version: 1.0
To: $DIRECCION_CORREO
From: $REMINTENTE
Subject: Verificación de consistencia - $FECHA_LINDA - $ERRORES
Content-Type: multipart/mixed; boundary="$SEPARADOR"

--$SEPARADOR
Content-Type: text/plain; charset=UTF8; format=flowed
content-transfer-encoding: 8bit

$ERRORES

Reporte generado al $FECHA_LINDA
Archivo adjunto: $ARCHIVO_SALIDA.pdf

Tamaño actual del directorio: $ACTUAL_SIZE

--$SEPARADOR
Content-Type: application/pdf; name="$ARCHIVO_SALIDA.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="$ARCHIVO_SALIDA.pdf"

$REPORTE

--$SEPARADOR
Content-Type: text/plain; name="$ARCHIVO_SALIDA.log"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="$ARCHIVO_SALIDA.log"

`cat $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log`

EOF
    done

}

Como nuestro archivo de salida es un archivo binario (PDF) no lo podemos incluirlo directamente, el mensaje de correo solo puede estar constituido por text ASCII. Para conseguir esto codificamos el archivo en base64 y separamos el mensaje en secciones, para eso nos servirá la variable SEPARADOR. En la primera sección pondremos el cuerpo del mensaje, en las siguientes colocamos los archivos "atados".

Veamos esto con mas detalle: 

En la linea 47 tenemos 
Content-Type: multipart/mixed; boundary="$SEPARADOR", que dice que el mensaje está formado por varias partes, de tipos mezclados y que el 'límite' entre las secciones será el texto de SEPARADOR.

En la linea 49 definimos el límite de la primera sección, notese que todos los límites empiezan con dos signos menos seguidos: "--",

En las siguientes lineas tenemos:
Content-Type: text/plain; charset=UTF8; format=flowed
content-transfer-encoding: 8bit

Define la sección como texto plano, utilizando set de caracteres UTF8 y que la codificación de la transferencia será de 8bits (http://www.freesoft.org/CIE/RFC/1521/5.htm).
Agregamos al texto del cuerpo algunas de los datos que habíamos colectado, ERRORES y TAMANHO_ACTUAL. En la linea 60 se define el límite de otra sección, esta vez el de nuestro archivo de salida binario. La cabecera de la sección está definida por:
Content-Type: application/pdf; name="$ARCHIVO_SALIDA.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="$ARCHIVO_SALIDA.pdf"

Definimos que el tipo de contenido va a ser application/pdf y el nombre del atado, se especifica que la codificación de transferencia será base64, que es un atado y el nombre de archivo sugerido al guardar. Y en la linea 65 colocamos el texto que goardamos en REPORTE.


Por último, en la linea 67 tenemos el límite de la última sección, seguido de la siguiente cabecera:

Content-Type: text/plain; name="$ARCHIVO_SALIDA.log"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="$ARCHIVO_SALIDA.log"



Como nuestro archivo de log está en formato texto, solo lo vamos a "volcar" a la sección con`cat $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log` y la cabecera especifica que es un atado.


Ahora nos encontramos con el código principal de nuestro script:

# generar el reporte
HOY=`date +"%d/%m/%y"`
java -cp $GXCLASSPATH achequeodesatendido "$HOY" "$HOY" "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA" $> "$DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.log"


# si se generó el archivo de salida, lo enviamos.
if [ -f $DIRECTORIO_SALIDA/$ARCHIVO_SALIDA.pdf ]; then
    echo "Existe $ARCHIVO_SALIDA"
    enviar_mail
fi



A pesar de ser el código principal, no hay mucho que explicar aquí, formateamos la fecha actual de la forma que espera nuestro programa como parámetro, llamamos a nuestro programa y redirigimos la salida estandar al archivo de log.


Finalmente verificamos si el programa generó alguna salida y la enviamos por mail con la función que explicamos mas arriba.


Probamos nuestro script:

sh test.sh


Si todo fue bien, debemos de estar recibiendo un mail con nuestros archivos como atados.




Agendar para ejecución periódica

Tan solo nos falta programar una tarea con CRON para que se ejecute periódicamente nuestro autómata.


Ejecutamos crontab -e y agregamos la siguiente linea:
*/5 * * * * sh /opt/chequeodesatendido/run.sh

Guardamos la tarea (presionamos ESC, luego :w + ENTER, por último :q + ENTER para salir). A partir de ahora estaríamos recibiendo un correo electrónico cada 5 minutos con los resultados de nuestro programa como atado.


Bueno, hasta aquí esta segunda entrega de la serie.


Autómatas con Genexus Ev1 en Linux y Windows, y I