Quantcast
Channel: Planeta Código
Viewing all 2713 articles
Browse latest View live

Poesía Binaria: Configurar Postfix para enviar correo a través de otro servidor SMTP [Gmail, sendgrid, mailgun y más]

$
0
0

Es una configuración muy común para los servidores de correo saliente. Sirve, por ejemplo para cuando queremos que los correos que enviemos desde nuestro ordenador se hagan a través de otro servidor SMTP, cuando hemos contratado un servicio externo para enviar correo para nuestra empresa, hacer redirecciones temporales o tener un servidor intermedio a la hora de recibir correos (por ejemplo, si queremos aplicar filtros antes de enviar los correos).

Por eso en el título de la entrada está Gmail. Podemos configurar un pequeño servidor de pruebas, o incluso en nuestra máquina local, para que los correos que se envíen con sendmail, o que nos envíen a nuestra máquina, se hagan a través de nuestra cuenta de Gmail (debemos activar el acceso por SMTP, y tendremos que tener cuidado de escuchar solo en localhost o poner reglas de firewall para que no todo el mundo pueda acceder a nuestro puerto de envío de correo). También están los casos de sendgris y mailgun, servicios que podemos utilizar para enviar correos desde un dominio determinado. Estos servicios están bien si desde nuestro servidor se envía un gran volumen de correo (existen muchos más servicios similares).

La otra opción es enviar un correo a través de nuestro propio servidor. Esto puede ser útil en caso de tener varios servidores de envío. Podemos hacer que varios servidores reciban correo y lo envíen a un servidor central. Si uno de los servidores pequeños ve que el central está muy saturado, puede esperar y entregarlo dentro de un rato. También es muy útil la creación de filtros para una empresa, es decir, la empresa tiene contratado el servicio de envío de correo a través de un proveedor externo, pero nosotros queremos tener algo de control como aplicar filtros, revisar virus, o eliminación de metadatos, o que debamos saber si hablan mal de nosotros, que para eso somos administradores de sistemas, aunque no te diré cómo, pero no es muy difícil.

Iré explicando el proceso desde el principio, por lo que si ya has instalado postfix o tienes cierta experiencia con él, podrás saltarte algunas partes. No profundizaré mucho en la configuración de Postfix porque es un mundo, solo en lo que nos atañe para esta tarea.

Instalación de Postfix

Para estos ejemplos he utilizado un equipo con Ubuntu Server 16.04, por lo que vais a ver cosas como apt para instalar los programas. No será muy diferente a hacer todo esto en una Debian, o cualquier otra distribución basada en Ubuntu o Debian. Puede que para otras distribuciones difiera un poco, pero la idea básica es la misma. Lo primero, es instalar Postfix:

sudo apt install libsasl2-modules ca-certificates postfix

Tras eso, veremos una ventana como la siguiente. Aquí podemos elegir configurarlo como Satélite. De hecho, nuestro servidor no hará el envío de correo de forma directa, sino a través de otra máquina.
Postfix como Satélite

La siguiente pregunta del instalador, es el nombre del host. Éste debe ser un FQDN, vamos un nombre que identifique la máquina y que su DNS resuelva. De forma que si desde otra máquina hacemos ping o telnet, la máquina la encontremos y sea esta máquina a la que estamos accediendo:
Elegir dominio de Postfix

El siguiente paso, será especificar el servidor a través del cual vamos a enviar los correos realmente:
Postfix, indicando el servidor Relay

En este punto debemos especificar el host y el puerto. Os pongo aquí varios hosts y puertos de algunos servicios comunes:

  • Gmail. smtp.gmail.com:25
  • Sendgrid. smtp.sendgrid.net:587
  • Mandrill. smtp.mandrillapp.com:587
  • Sparkpost. smtp.sparkpostmail.com:587
  • Mailgun. smtp.mailgun.org:25

O incluso podemos utilizar cualquier otro servidor de correo de otro proveedor (que hay cientos de servicios gratuitos y de pago para esto). Puede que para algunos de los servicios haya que realizar una activación previa del servicio SMTP, o incluso configurar DNS para poder trabajar correctamente.

Por último, debemos revisar, en el fichero /etc/postfix/main.cf que la línea que menciona myhostname tiene nuestro nombre de host de correo (el FQDN que introdujimos en la segunda pantalla).

Configurar servidores SMTP, usuarios y contraseñas

Cualquier servidor hoy en día, debe tener activada la autentificación de usuarios, de modo que no permita que cualquiera envíe correos desde ese servidor. Era muy habitual en el pasado encontrar servidores abiertos desde los que poder enviar cualquier cosa y, tanto en el mundo fantástico y maravilloso de la piruleta, como en el diseño original del sistema de correo debía ser así. El problema es que mucha gente aprovechaba para hacer cosas malas.

El primer acceso que vamos a configurar es el que va desde el servidor de correo Postfix que estamos montando hasta el servidor de correo a través del cual vamos a enviar los mensajes. Para ello creamos el archivo /etc/postfix/sasl_passwd, en realidad, podemos llamarlo como queramos, aunque es una buena idea colocarlo dentro de /etc/postfix. Dentro del archivo incluimos lo siguiente (con los corchetes y todo):

1
[mail.dominio.com]:puerto usuario:contraseña

En esta línea incluiremos el nombre del servidor desde el que realmente enviaremos los correos (Gmail, sendgrid, mailgun…), el puerto, y el nombre de usuario y contraseña desde el que enviaremos los correos. Si, por ejemplo, estamos utilizando nuestra dirección de Gmail para enviar correo, solo podremos utilizar como remitente nuestra propia dirección, aunque tienen por ahí una opción para poder utilizar más direcciones de correo de Google de esta forma. Otros servicios permiten, a través de una clave única enviar correos desde cualquier dirección de correo dentro de un dominio o subdominio, deberá ser nuestro Postfix el que determine qué un usuario no pueda enviar correo con el e-mail de otro.

Una vez tenemos /etc/postfix/sasl_passwd creado, vamos a crear una tabla de búsqueda (lookup-table) para Postfix de esta forma:

sudo postmap /etc/postfix/sasl_passwd

De esta forma creamos el archivo /etc/postfix/sasl_passwd.db . Lo siguiente será proteger los dos archivos de contraseñas, tanto el que no tiene extensión (que, si queremos lo podemos borrar y no pasa nada, o también custodiarlo en otra máquina o con otro usuario, ya como queramos). Para protegerlo, hacemos que solo root sea capaz de acceder a los archivos:

sudo chown root:root /etc/postfix/sasl_passwd.db /etc/postfix/sasl_passwd
sudo chmod 0600 /etc/postfix/sasl_passwd.db /etc/postfix/sasl_passwd

Aunque, si sois frikis como yo, y no queréis escribir mucho podéis hacer lo siguiente (más info aquí):

sudo chown root:root /etc/postfix/sasl_passwd{,.db}
sudo chmod 0600 /etc/postfix/sasl_passwd{,.db}

Por último, tenemos que configurar nuestro servidor relay editando el archivo /etc/postfix/main.cf , primero añadiendo (o verificando) que el relay host está configurado, es decir, el servidor a través del cual enviamos realmente los correos:

1
relayhost=[mail.dominio.com]:puerto

Podemos poner o buscar esta información cerca de myhostname, mydestination, mynetworks.

Ahora, en el mismo archivo, por ejemplo, al final del archivo, podemos poner un comentario y a continuación las siguientes líneas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Configuración de seguridad para el servidor saliente

# Usas SASL para autentificarnos antes de enviar correos
smtp_sasl_auth_enable= yes

# Utilizar el mapa /etc/postifx/sasl_passwd.db
smtp_sasl_password_maps= hash:/etc/postfix/sasl_passwd

# No permitr métodos de autentificación anónima
smtp_sasl_security_options= noanonymous

# Utilizar el fichero de certificados raíz del sistema
smtp_tls_CAfile= /etc/ssl/certs/ca-certificates.crt

# Utilizar STARTTLS para enviar los correos
smtp_use_tls= yes

Configurar autentificación en nuestro servidor

Nuestro servidor debe identificarse cuando se conecta para mandar correos a través de otra máquina (como vimos, que puede ser Gmail, Sendgrid, Mailgun, etc). Pero claro, puede que queramos que nuestros usuarios también se identifiquen en nuestro servidor cuando se conecten. De esta forma, cualquier persona no podrá enviar e-mails. Es algo que no es fundamental, pero sí recomendado si no queremos que nuestro servidor se transforme en una máquina zombie que envíe correos sin piedad, lo que puede desembocar en que el servidor de envío a través del cual mandamos los mensajes nos termine desactivando el usuario y, si el dominio es nuestro, éste pierda reputación y nos resulte tremendamente difícil que el mundo no se crea que enviamos SPAM.

Para configurar esta seguridad, vamos a crear una pequeña base de datos local con los usuarios permitidos. Seguro que si tenemos miles de usuarios deberíamos tener métodos de autentificación más grandes y distribuidos, pero, por experiencia en la mayoría de los casos, utilizando SASL, como antes, va muy bien.

Instalaremos algunas utilidades, si no las tenemos ya:

sudo apt install libsasl2-2 sasl2-bin

Ahora, debemos asegurarnos de que el demonio saslauthd se ejecuta automáticamente al iniciar el sistema. Para ello, editaremos /etc/default/saslauthd y nos aseguremos de que la línea que comienza por START y por MECHANISM se mantiene así:

1
2
3
4
5
6
# Should saslauthd run automatically on startup? (default:no)
START=yes
.....
.....
# Example MECHANISMS="pam"
MECHANISMS="sasldb"

Tras ello, reiniciaremos el demonio:

sudo service saslauthd restart

Ahora debemos configurar de nuevo algunas cosas en /etc/postfix/main.cf:

1
2
3
4
5
smtpd_sasl_auth_enable= yes
smtpd_sasl_local_domain=
smtpd_recipient_restrictions= permit_sasl_authenticated,
        reject_unauth_destination
smtpd_sasl_security_options=noanonymous

Tras esto, debemos crear (o editar) el archivo /etc/postfix/sasl/smtpd.conf donde especificaremos el tipo de autentificación permitida a la hora de conectarnos e intentar enviar correo a través de Postfix. Dejaremos las siguientes líneas:

1
2
pwcheck_method: saslauthd
mech_list: PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM

Por supuesto, podríamos (y deberíamos) eliminar PLAIN sobre todo, para que nadie pueda identificarse con contraseñas en texto plano. Aunque, la práctica me dice que a veces puede que necesitemos activarlo temporalmente. De hecho, es problema de los usuarios si utilizan PLAIN para identificarse, los clientes de correo siempre intentan identificarse con el método más seguro disponible.

Tras todo esto, reiniciamos Postfix:

sudo service postfix restart

Por último, podemos tener un pequeño problema a la hora de identificar usuarios, y es que, Postfix, muchas veces está configurado con un chroot o una jaula. Eso quiere decir que postfix será incapaz de ver archivos fuera de su jaula, como por ejemplo, el socket de identificación de saslauthd, por lo tanto, no podrá identificar correctamente a los usuarios. Para ello, solo tenemos que introducir el directorio /var/run/saslauthd dentro de la jaula, de esta forma:

sudo mount --bind /var/run/saslauthd /var/spool/postfix/var/run/saslauthd

Este comando deberíamos ejecutarlo siempre que se inicie el sistema, podríamos incluir una línea en /etc/fstab o un script de arranque personalizado.

Manejo de usuarios

Si queremos crear o cambiar la contraseña de los usuarios del sistema deberíamos hacer:

sudo saslpasswd2 -c [nombre de usuario]

O también

sudo saslpasswd2 -c [nombre de usuario]@[dominio]

Para eliminar un usuario podremos utilizar -d en lugar de -c y para listar todos los usuarios del sistema tenemos el comando:

sudo sasldblistusers2

Restringir dispositivos de red

Esto depende de dónde se van a enviar los mensajes. Por seguridad es una buena opción restringir los interfaces de red permitidos para cada servicio que instalamos en nuestros servidores, haciendo que los servicios solo escuchen en aquellos dispositivos para los que estén ofreciendo servicio. Así que, en Postfix también podemos decidir esto. En /etc/postfix/main.cf debemos configurar:

1
inet_interfaces= all

Con esta configuración estaremos escuchando en todos los dispositivos de red que tenga la máquina. Si lo hacemos así, debemos restringir el acceso a puertos (si queremos) a otros usuarios. Podríamos utilizar, en lugar de all, la opción loopback-only si solo escuchamos en localhost. Esto puede ser útil si tenemos un servicio web instalado en este servidor que necesite enviar correos. También podemos poner las direcciones IP o nombres de host desde los que escuchamos separados por comas (si son varios).

Si cambiamos algo aquí tendremos que reiniciar el servidor postfix.

Enviando un mensaje

Es momento de enviar un mensaje de prueba. Tenemos muchas opciones. Podemos utilizar sendmail de la siguiente forma:

/usr/sbin/sendmail correo@destinatario.com
From: direccion@origen
To: correo@destinatario.com
Subject: Asunto del mensaje
Texto del mensaje, que puede tener.
muchas líneas y ser muy largo.

Tras ello, podemos pulsar Control+D y terminaremos de escribir el mensaje. Supuestamente debe haberse enviado si todo está correcto.

También podemos tirar de funciones de varios lenguajes de programación como mail() para PHP. O de scripts como gemail.sh con el que se podrán enviar archivos adjuntos de forma fácil. Por supuesto, también podemos configurar nuestro cliente de correo favorito para enviarlos.

Logs y depuración

Y, como todo en este mundo informático puede fallar, es importante tener a mano los logs. En estos logs podremos ver mensajes que nos dirán qué está pasando en cada momento. Tendremos dos /var/log/mail.log que nos dirá qué está pasando con cada mensaje, IDs de correos, mensajes que entran en cola o no, errores al mandar un mensaje a otro servidor o al recibirlo, etc. También podemos encontrar errores en /var/log/mail.err sobre todo en lo relativo a fallos en el demonio, conexiones que no se han podido establecer o intentos de hacer cosas raras como por ejemplo poner muchas líneas de encabezados en un mensaje (podemos tener 20, 30 líneas, pero cuando alguien intenta meter 100 o 200 líneas, puede que esté intentando hacer algo malo, o que no esté enviando bien el mensaje).

Foto principal: Designed by Freepik

The post Configurar Postfix para enviar correo a través de otro servidor SMTP [Gmail, sendgrid, mailgun y más] appeared first on Poesía Binaria.


Variable not found: Generar redirecciones HTTP 307 y 308 en ASP.NET Core MVC

$
0
0
ASP.NET Core MVCHace unos días hablábamos de las, a veces desconocidas, redirecciones HTTP 303, 307 y 308, y comentábamos el por qué de su existencia y los escenarios en los que debían ser utilizadas si queremos seguir de forma más estricta el protocolo HTTP a la hora de implementar APIs o aplicaciones.

Pues bien, al hilo de esto, en este post veremos muy rápidamente cómo podemos implementar estas redirecciones en ASP.NET Core MVC que, como veréis, nos lo pone bastante fácil.

En ASP.NET Core MVC, las redirecciones se implementan básicamente retornando resultados de tipos como RedirectResult, LocalRedirectResult, RedirectToActionResult o RedirectToRouteResult. El fin de todos ellos es el mismo: retornar al lado cliente una respuesta con el código HTTP 3xx indicando en el encabezado location la dirección del recurso donde se encuentra el recurso solicitado.

Normalmente no instanciaremos estas clases directamente, sino que utilizaremos los atajos proporcionados por la clase Controller, como en el siguiente ejemplo:
publicclass RedirectionTestController: Controller
{
public IActionResult Temporal()
{
// Returns HTTP 302 pointing to /redirectiontest/ok
return RedirectToAction("Ok");
}

public IActionResult Ok()
{
return Content("Ok!");
}
}
En la clase Controller existen multitud de métodos destinados a facilitar la construcción de retornos de los distintos tipos redirección, que utilizaremos dependiendo de cómo queramos indicar el destino de las mismas. Por ejemplo:
  • El método Redirect() permite generar redirecciones hacia URL arbitrarias.
  • RedirectToAction() nos permitirá generar redirecciones hacia controladores/acciones de nuestra aplicación.
  • RedirectToRoute() generará redirecciones partiendo de los parámetros de ruta especificados.
  • RedirectToPage() genera redirecciones hacia páginas Razor.
  • LocalRedirect() permite generar redirecciones hacia recursos locales, pertenecientes al mismo sitio web.
Por defecto estas redirecciones serán transitorias (HTTP 302), aunque cada uno de estos métodos presenta una variante, cuya denominación acaba en “Permanent”, que permite generar redirecciones permanentes (HTTP 301), como en el siguiente ejemplo:
publicclass RedirectionTestController: Controller
{
...
public IActionResult Permanent()
{
// Returns a permanent redirection (HTTP 301) to /redirectiontest/ok
return RedirectToActionPermanent("Ok");
}
}
Petición POST con redirección HTTP 307De esta forma, mediente llamadas a métodos como RedirectToActionPermanent(), RedirectToRoutePermanent() o RedirectToPagePermanent() podremos crear objetos IActionResult que, al ejecutarse, generarán redirecciones permanentes.

Pues bien, observad que hasta ahora sólo hemos hablado de redirecciones HTTP 301 y 302, que son las más habituales, pero ASP.NET Core MVC también nos ofrece facilidades para implementar los códigos 307 y 308.

Como sabemos, lo que estos dos tipos de redirección aportan sobre los conocidos HTTP 301 y 302 es, básicamente, que con ellos indicamos explícitamente al agente de usuario que la petición a la dirección indicada en el encabezado location debe ejecutarse respetando el verbo original.

Por ello, si observamos las ayudas e intellisense cuando vamos a retornar una redirección observaremos que, por cada uno de los métodos de generación de redirecciones que hemos comentado antes, existe un método adicional cuyo nombre finaliza por “PreserveMethod”.

Por ejemplo, en el siguiente bloque de código utilizamos estos métodos para generar redirecciones a acciones con códigos 307 y 308:
public IActionResult TemporalPreserve() 
{
// HTTP 307, temporal redirect preserving method
return RedirectToActionPreserveMethod("Ok");
}
public IActionResult PermanentPreserve()
{
// HTTP 308, permanent redirect preserving method
return RedirectToActionPermanentPreserveMethod("Ok"); //
}
Y de nuevo, tendremos este sufijo en prácticamente todos los métodos que generan resultados de redirección, ya sean transitorios o permanentes: RedirectPreserveMethod(), RedirectToRoutePreserveMethod(), LocalRedirectPermanentPreserveMethod(), etc., por lo que siempre tendremos a mano la posibilidad de optar por unos códigos de retorno u otros y ser más explícitos en las respuestas.

Hey, ¿y los métodos y clases para generar redirecciones HTTP 303?

Pues me temo que se quedaron en el camino :(

De momento no existen formas directas de generar este tipo de redirecciones con las herramientas proporcionadas de serie en el framework. Supongo que el motivo se deberá a que se entiende que su uso no será frecuente y, supongo también, para no “ensuciar” la clase Controller con demasiadas variantes de métodos de generación de redirecciones.

Pero bueno, esto no quiere decir que sea difícil conseguirlo: obviamente nada nos impide retornar un StatusCode(303) o bien un  crear nuestro propio IActionResult personalizado para este tipo de redirecciones, algo que no se antoja demasiado complicado.

Venga, os lo dejo como deberes ;)

Publicado en Variable not found.

Blog Bitix: La clase Optional de Java para evitar la excepción NullPointerException

$
0
0

Con la clase Optional añadida en el JDK en la versión 8 del lenguaje Java se puede evitar una de las excepciones más comunes que se produce cuando se hace uso de una referencia nula a un objeto.

Java

Una de las excepciones que más se producen en un programa hecho con el lenguaje Java es la conocida NullPointerExpcetion que ocurre cuando se hace uso de una variable que referencia a un objeto pero que el contenido de la variable es null, sin valor o sin contener una referencia a un objeto de modo que la llamada al método no es posible. La excepción NullPointerException extiende de RuntimeException por lo que es una unchecked exception y por ello no es necesario capturarla o lanzarla, cuando se produce hay un error en el programa.

Entre las novedades que Java 8 incluyó en el lenguaje y JDK está la inclusión de la clase Optional con la que haciendo uso de ella se pueden evitar los NullPointerException. Un objeto de tipo Optional contiene o no una referencia a otro tipo de objeto. Por ejemplo, una variable de tipo Optional<String> contiene una referencia a un objeto Optional que a su vez contiene o no una referencia a una cadena String. El uso de la variable Optional no producirá un NullPointerException y con sus métodos es posible saber si contiene o no una referencia al tipo usado en el genérico. Con el método isPresent() es posible saber si el Optional contiene una referencia, con orElse() y orElseGet() se obtiene la referencia que indiquemos en caso de que no tenga una referencia y con los métodos estáticos a modo de constructores of() y ofNullable() se obtiene respectivamente una instancia de Optional con la referencia indicada que no puede ser nula o un Optional que podría contener una referencia nula.

Usar un objeto Optional advierte al programador de que la referencia que contiene puede ser nula y usada correctamente evita los NullPointerException aunque usarla indiscriminadamente hace del código más incómodo de escribir y leer, en variables locales es prescindible su uso pero útil en algunos valores de retorno o parámetros de métodos.

Hay algunos métodos más en la clase Optional con algunas funcionalidades adicionales que hace uso de las nuevas capacidades funcionales del lenguaje como convertir un Optional a un Stream.

Navegapolis: Scrum para mejorar el desarrollo de prototipos en la manufactura industrial

$
0
0
automovilJoe Justice, tras años de trabajo en el desarrollo y gestión de proyectos de software fundó Wikispeed en 2006, un innovador proyecto para desarrollar prototipos de automóviles aplicando los principios de scrum, TDD y programación orientada a objetos.
 
Combinando estos métodos Wikispeed construyó en 3 meses un automóvil modular con un consumo de 2,3 litros a los 100 kmts. Actualmente Wikispeed trabaja en proyectos como el desarrollo de un modelo de furgoneta para servicios postales a nivel mundial, un taxi de nueva generación, un automóvil de carreras ultraligero y un sedán de 5 plazas.

 

 

De todas formas no debería sorprendernos el uso de scrum en la manufactura industrial. Fue allí donde surgió ;-)

Blog Bitix: Personalizar el prompt del sistema del intérprete de comandos Bash

$
0
0
GNU
Linux

El símbolo de sistema o prompt de la terminal es el símbolo del sistema que precede al comando que introducimos en la terminal y por defecto indica el directorio de trabajo que utilizarán los comandos y el directorio que utilizarán las rutas relativas. El prompt del intérprete de comandos Bash se puede personalizar para por ejemplo modificar el color de su texto y la información que incluye con las preferencias del usuario.

El prompt se configura con la variable de entorno PS1 que en bash está en el directorio personal y el archivo .bashrc. Los colores se especifican con una secuencia de caracteres y números y la información a mostrar se especifica usando caracteres de escape seguidos de diferentes letras. Como conocer las secuencias de caracteres con los códigos de escape ANSI para los colores y letras para la información es complicado de escribir o conocer al detalle en la web Bash $PS1 Generator hay un asistente que facilita el crear un prompt a nuestro gusto correcto.

El siguiente es el que utilizo en mi sistema con colores, el nombre del usuario, el nombre sistema y el directorio de trabajo actual.

Prompt de la terminal personalizado

Según se edita el prompt el asistente previsualiza como queda en la sección Preview, una vez que el prompt es el que deseamos en la sección Result el asistente ofrece el contenido de la variable de entorno PS1 que hay que añadir o cambiar en el archivo ~/.bashrc.

Si quieres mostrar alguna información que sea el resultado de algún comando también se puede añadir al prompt. Por ejemplo, supón que se desea añadir la hora del sistema al prompt, esta información la devuelve el comando date. El siguiente script de bash usando el comando date proporciona la información a añadir.

Ahora hay que incluir esta información en el prompt de la siguiente forma.

Prompt de la terminal personalizado con el resultado de un comando

Una caso habitual es añadir la rama de git del directorio actual de trabajo y si esta tiene cambios como comento en Prompt de la terminal personalizado en carpetas de git con el intérprete Bash. El script necesario que proporciona Git no es más complejo que el caso anterior pero con la misma finalidad, el script está en git-prompt.sh y requiere modificar la variable de entorno PS1 o bien PROMPT_COMMAND.

Prompt de la terminal personalizado en carpeta de git

Variable not found: Enlaces interesantes 319

$
0
0
Enlaces interesantes
Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Otros

Publicado en Variable not found.

Poesía Binaria: Píldora: Cómo buscar un texto dentro de documentos múltiples ODT de Libreoffice/Openoffice o docx de Microsoft Word

$
0
0

Cuántas veces has recordado haber escrito un documento y no recuerdas dónde lo pusiste. Tienes cientos de archivos de documentos en tu disco duro, o en un servidor y no sabes por dónde empezar a buscar. Con ficheros de texto, podemos utilizar find, grep, egrep, sed y algunos comandos más que, combinados podrán darnos los resultados que buscamos. Aunque los archivos de documentos son algo más complejos internamente.

Ficheros de documentos

Tanto los ficheros de OpenOffice/LibreOffice como los documentos de Microsoft Word Open XML (esos archivos docx que se crean a partir de Microsoft Office 2007. En este post no podremos buscar documentos generados con versiones más antiguas del programa) son en realidad archivos ZIP con muchos archivos XML en su interior en los que se definen contenido, estilos y metadatos. Para los archivos odt, de LibreOffice y OpenOffice, el archivos que contiene los contenidos del documento es content.xml y en Microsoft Word, el archivo está situado en word/document.xml, así que, vamos a intentar, buscar todos los archivos de documentos que haya en una ruta del disco y examinar su contenido para buscar las palabras que necesitamos encontrar. Para ello tendremos que descomprimir el documento y extraer el archivo del documento. Todo de forma automática.

Es cierto que actualmente tenemos programas de búsqueda en escritorio, que examinan periódicamente archivos en varios formatos, procesan dicha información y agilizan la muestra de resultados. Aunque en muchos casos, para acelerar el uso normal del ordenador, muchos los solemos tener desactivados. O, por ejemplo, si tienes los contenidos en un ordenador remoto, una Raspberry PI, o un VPS, puede resultar un poco más complicado hacer dichas búsquedas.

Afortunadamente GNU/Linux nos proporciona herramientas muy buenas para realizar nuestras tareas. Primero ejecutaremos find para obtener todos los archivos de documento a partir de la ruta actual. Aunque podríamos utilizar find con otros parámetros o incluso utilizar locate. Para ello, podemos utilizar estos scripts de una línea, también llamados one-liners.

Documentos de LibreOffice/OpenOffice

Empezaremos con los documentos odt de LibreOffice u OpenOffice. Para ello extraeremos el archivo word/document.xml del archivo de documento y a partir de ahí realizaremos una búsqueda.

find -name ‘*.odt’ | while read file; do unzip -p “$file” “content.xml” | grep -li “TEXTO A BUSCAR” > /dev/null; if [ $? -eq 0 ]; then echo $file; fi; done

De esta forma, find, va pasando archivos odt al resto de la línea. Podríamos hacer fácilmente que no sean todos los archivos, si nos acordamos de parte del nombre podríamos hacer ‘*informe*.odt’ para seleccionar todos los docuentos que contentan la palabra informe y tengan extensión odt; o incluso utilizar -iname en lugar de -name para indicar que busque nombres de archivo en mayúsculas y minúsculas indistintamente. También, con find podríamos seleccionar incluso los archivos por tamaño o por fecha.

En lugar de find -name ‘*.odt’ podríamos utilizar locate ‘*.odt’ para buscar en todo el disco, o en un patrón de directorios determinado. La ventaja de locate es su gran velocidad, aunque primero tendremos que crear una base de datos de archivos del disco (con updatedb) y eso puede tardar un poco.

Una vez tenemos los nombres de archivo que cumplen el patrón, cogemos cada nombre de archivo y ejecutamos unzip -p [archivo] content.xml. La opción -p sirve para extraer el contenido de los archivos directamente a la salida estándar de la aplicación. Si ejecutamos este comando individualmente, veríamos todos los contenidos del fichero en pantalla, vamos, extrae todo a pantalla, en lugar de crear un archivo llamado content.xml. La ventaja de esto es que no necesitamos archivos temporales y todo es mucho más rápido.

La salida de unzip, es decir, el contenido del fichero comprimido, se lo pasamos a grep. En este caso, grep -li “TEXTO” > /dev/null. Con -l, hacemos que grep muestre solo los nombres de archivo donde exista coincidencia, así sólo es necesario que el texto se encuentre una vez en el archivo para que grep finalice la ejecución y devuelva un positivo. Lo malo es que si el texto no está en el archivo tendrá que analizarlo completamente. El argumento -i hace que no importen las mayúsculas y minúsculas, por lo que, aunque en el documento encontremos “CaCaHueTe”, podremos decir que grep busque “cacahuete” y lo va a encontrar sin problema. Por último, la salida de grep la redirigimos a /dev/null para que realmente no muestre nada en pantalla. ¿Por qué?

Lo que en realidad analizaremos de grep será el estado de salida del programa. Es decir, internamente, si grep acaba y el archivo que ha analizado contiene el texto que buscamos, devolverá un 0 (normalmente, cuando un programa finaliza bien devuelve un 0 y cuando no, devuelve otra cosa). Entonces, sólo si grep devuelve 0 mostraremos el nombre del archivo en pantalla. Para eso if [ $? -eq 0 ]; then echo $file; fi;.

Documentos de Microsoft Word

Para hacer lo mismo con los documentos de Microsoft Word debemos cambiar el archivo que analizamos. En este caso, analizaremos word/document.xml. Y mantendremos todo el comando igual:

find -name ‘*.docx’ | while read file; do unzip -p “$file” “word/document.xml” | grep -li “TEXTO A BUSCAR” > /dev/null; if [ $? -eq 0 ]; then echo $file; fi; done

Mejorar la búsqueda

Los ficheros que estamos analizando, en realidad son XML, por lo que, junto con el contenido encontramos algunos datos que nos indicarán estilos, objetos empotrados, notas, y demás cosas. Todo esto puede hacer que cuando busquemos algún contenido concreto no lo encontremos. Para mejorar la búsqueda, aunque penalizaremos un poco el tiempo que tardará el script, sobre todo en archivos muy grandes. La clave está en eliminar las etiquetas XML que encontramos en el archivo de contenidos. Un claro ejemplo, podemos verlo si analizamos un fichero HTML. Cuando vemos:

Hola Mundo

En realidad, el código utilizado para el texto es este:

1
<strong>Ho</strong>la Mun<strong>do</strong>

Y si realizamos una búsqueda en texto con grep, por ejemplo, buscando la palabra “Hola”. Grep no encontrará nada. Así que, pasaremos el texto por un filtro que eliminará estas etiquetas, con sed. Uno de los usos de este comando es reemplazar cadenas a través de expresiones regulares, pero aunque suene complicado, os la voy a dar preparada. Para ver un ejemplo rápido, vamos a hacer lo siguiente:

echo “Hola Mundo” | grep -i hola

Esto no devolverá nada, porque, como hemos dicho antes, no vamos a encontrar nada en la cadena anterior. Pero,
echo “Hola Mundo” | sed -e ‘s/]*>//g’ | grep -i hola
Hola Mundo

El segundo ejemplo sí que devuelve el contenido. El objetivo es sustituir globalmente (s/búsqueda/reemplazo/g) cualquier texto que esté entre < y > (<[^>]*< por un texto en blanco.

Creando el script completo

En definitiva, para buscar archivos de LibreOffice / OpenOffice, esos con extensión ODT:

find -name ‘*.odt’ | while read file; do unzip -p “$file” “content.xml” | sed -e ‘s/]*>//g’ | grep -li “TEXTO A BUSCAR” > /dev/null; if [ $? -eq 0 ]; then echo $file; fi; done

Y para buscar archivos Office Open XML de Microsoft Word, esos con extensión DOCX:

find -name ‘*.docx’ | while read file; do unzip -p “$file” “word/document.xml” | sed -e ‘s/]*>//g’ | grep -li “TEXTO A BUSCAR” > /dev/null; if [ $? -eq 0 ]; then echo $file; fi; done

Foto principal: unsplash-logoDmitry Ratushny

The post Píldora: Cómo buscar un texto dentro de documentos múltiples ODT de Libreoffice/Openoffice o docx de Microsoft Word appeared first on Poesía Binaria.

Picando Código: DATA cumple 6 años 🎂

$
0
0

Y llegó un día diferente a cualquier otro, cuando los héroes más poderosos de la sociedad civil uruguaya se unieron contra una amenaza común. Ese día nació DATA. Para luchar contra los datos impuros y falta de transparencia que ningún ciudadano de a pié sólo podría combatir.

DATA - 6 años

DATA es un grupo de rebeldes aglomerado bajo una organización de la sociedad civil fundada en abril de 2012 que trabaja en temas de gobierno abierto, datos abiertos, acceso a la información pública y participación a través del uso de tecnología cívica. Hoy 30 de abril de 2018 se cumplen nada menos que 6 años de su nacimiento. Pensar que hace poquitofestejábamos el primer año. ¡Feliz cumpleaños DATA!

Como parte de la celebración, DATA publicó una nueva edición de su boletín para ver en qué andan. Copio y pego la información a continuación:

DATA NOTICIAS
¿Qué hay de nuevo?

¡Hola! Les damos la bienvenida a nuestro boletín de noticias.

Para empezar queremos contarles que hoy lunes 30 de abril, Data Uruguay cumple 6 años 🎂, y nos pareció que era una buena oportunidad para retomar nuestros boletines.

El 2017 fue un año de mucho trabajo y de muchos proyectos. Un año para seguir conociendo gente increíble que comparte las mismas locuras en distintas partes del mundo y para seguir consolidando la organización.

Queremos compartir con ustedes esta publicación donde están algunos de los principales hitos del año pasado, los proyectos que lanzamos, los eventos que organizamos, acompañamos y asistimos y algunas noticias que nos alegraron, tanto propias como en colaboración con organizaciones amigas.
VER MÁS

¿EN QUÉ ANDAMOS?
Nuestros nuevos proyectos

4˚ Plan de Acción Nacional de Gobierno Abierto

2018 es el año de co-creación del 4º Plan de Acción Nacional de Gobierno Abierto y en el marco de ese proceso, por primera vez está disponible una herramienta web donde cualquier persona u organización puede hacer propuestas para el plan. En Data Uruguay trabajamos activamente en este proceso, como una de las organizaciones representantes de la Red de Gobierno Abierto.

Te invitamos a visitar la web, conocer las propuestas planteadas para el nuevo plan de acción y/o proponer las tuyas. Si te interesa esta temática, también podés mantenerte al tanto siguiendo a la RGA en facebook y twitter.

Trabajando en la promoción de derechos

Desde hace algunos meses estamos trabajando con la Dirección Nacional de Promoción Sociocultural (DNPSC) del MIDES para lanzar una herramienta que sirva para trabajar en los Centros Promotores de Derechos. Estos surgen con la intención de fortalecer y generar más y nuevas prácticas en promoción de derechos en centros educativos, abordando diferentes ejes temáticos: género, diversidad sexual, étnico-racial, salud adolescente, discapacidad, pertenencias, convivencia e itinerarios socioeducativos.

Esta plataforma, co-creada por MIDES y DATA Uruguay, permitirá contar con una herramienta virtual que incluya contenidos teóricos, videos para jóvenes y adultos/as, orientaciones metodológicas, dinámicas lúdicas y recursos  audiovisuales para la promoción de derechos.

Próximamente los/as estaremos invitando al lanzamiento 🙂

LO QUE SE VIENE
Próximas actividades y eventos

7 de MAYO.Compromisotón

En el marco de la Open Gov Week, desde la Red de Gobierno Abierto (red de la que DATA Uruguay forma parte) estamos organizando un “Compromisotón” para sumar insumos al proceso de de co-creación del 4º Plan de Acción Nacional de Gobierno Abierto.

Te esperamos el lunes 7 de mayo a las 18:30 hs. en Enlace (Av. Agraciada 2332).

¡Sumate al evento y al Meetup!

23 AGO.ABRE LATAM e IODC

El 27 y 28 de setiembre de 2018 se realizará en Buenos Aires la Conferencia Internacional de Datos Abiertos (IODC18 por sus siglas en inglés).

Este año Abrelatam, el ya clásico evento regional sobre Datos Abiertos, también será allí así que te invitamos a registrarte, hacer las valijas y acompañarnos 🙂

VER MÁS

EL MUNDO DE LOS DATOS ABIERTOS

Proyecto Educación

El año pasado recibimos la maravillosa noticia de que uno de nuestros proyectos había quedado seleccionado para formar parte de la primera generación Altec, un fondo de Omidyar Network y Avina.

Desde entonces estamos trabajando en el “Proyecto Educación” (este es nombre de trabajo, todavía no fue bautizado) junto a nuestros socios, el Centro de Educación Secundaria y Unicef.

La idea del proyecto es realizar un plataforma para visualizar datos de todos los liceos públicos del país. La herramienta será co-creada junto a los/as docentes, estudiantes y otros grupos de personas involucradas con la educación. ¡En los próximos meses se vienen novedades!

Novedades sobre ¿Dónde Reciclo?

¿Dónde Reciclo? fue una de las primeras herramientas que lanzamos desde Data Uruguay, allá por el 2013. El año pasado nos asociamos con Cempre y relanzamos la plataforma, mejorando el diseño, desarrollando app para iOS y Android y mejorando aspectos de los datos y la usabilidad.

Actualmente estamos empezando a trabajar en una nueva versión, con algunas mejoras, ¡así que estén atentos!

Por Mi Barrio en el interior

Desde que empezamos a trabajar con la plataforma Por Mi Barrio (PMB) había algo que queríamos hacer pero no habíamos tenido oportunidad: trabajar en todo el Uruguay, no sólo en la capital. Con ese foco estuvimos trabajando para replicar PMB en Rivera y felizmente la plataforma ya está en marcha (agradecemos a la Intendencia de Rivera, a Avina y a la CAF por todos los esfuerzos).

Hoy nos alegra mucho anunciar que gracias a un fondo de la ANII, PMB también estará funcionando en Río Negro, ya estamos trabajando con la Intendencia de ese Departamento para que suceda. En los próximos meses les estaremos contando novedades 🙂

¿Tenés ganas de ser voluntario? ¡SUMATE!
Escribinos


Bitácora de Javier Gutiérrez Chamorro (Guti): Remover, resumir y otros términos

$
0
0

Con frecuencia leo textos técnicos, mayoritariamente escritos por latinoamericanos que, quizás por la influencia del inglés son ciertamente incorrectos en ese contexto. Un poco como ya viéramos con Vectores, matrices, registros y otras hierbas, pero esta vez constatados sobre el DRAE (Diccionario de la Real Academia de la lengua Española). La importancia de usar las […]

Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

La entrada Remover, resumir y otros términos aparece primero en Bitácora de Javier Gutiérrez Chamorro (Guti).

Blog Bitix: Plantillas con etiquetas no balanceadas en Apache Tapestry

$
0
0

Apache Tapestry es uno de los mejores frameworks web para Java que he usado, sin embargo, también tiene algún incordio o curiosidad. Una de ellas es que las plantillas han de ser XML bien formado y en este caso que comento en el que necesitaríamos una plantilla con etiquetas desbalanceadas nos obliga a buscar una solución, esta es la que uso.

Apache Tapestry

Las plantillas en Apache Tapestry son XHTML, esto quere decir que al ser archivos xml han de estar bien formados con sus etiquetas bien balanceadas y anidadas. Esto tiene la ventaja de que Tapestry nos avisará cuando se intente generar código HTML mal formado con el contenido de plantilla, una especie de compilación que si no es correcta no pasará desapercibida como en otros frameworks y que puede generar otro tipo de errores. Sin embargo, también tiene una desventaja y es que si necesitamos generar una etiqueta de inicio dentro de un componente if la plantilla no será válida.

Supongamos que queremos generar un contenido a tres columnas de una serie de elementos, en cada tercer elemento de la fila tendremos que cerrar la fila anterior y abrir una nueva. El ejemplo de lo que no se puede hacer es el siguiente:

En este ejemplo las etiquetas div de apertura y cierre hacen que el XML de la plantilla no esté bien balanceado. No se si habrá otra forma mejor pero esta es el rodeo que uso para tener una plantilla bien balanceada y generar el contenido HTML necesario. En el código Java del componente creo un método que devuelve un mapa de trozos de HTML que no podría incluir en la propia plantilla, cada trozo de HTML tiene una clave asociada por la que identificarlo, en este caso open y close.

Usando estos métodos en la plantilla el código de la plantilla tml ya bien formado quedaría de la siguiente manera, usando el componente OutputRaw se emite el trozo HTML de apertura o cierre:

Esta pequeña «ñapa» que no es muy habitual en las plantillas pero que en algún caso puede ser necesario emplear es una forma de hacer las plantillas tml bien formadas en circunstancias donde incluyéndolo en la plantilla no lo sería.

Prefiero que Tapestry me valide que el XML de la plantilla esté bien formado evitando que se pudiera generar HTML con etiquetas no balanceadas y me obligue a hacer este rodeo que la posibilidad de generar HTML con etiquetas mal balanceadas que los navegadores aceptan pero que en algún caso podría provocar una desmaquetación, aunque en los casos que se aplique esta solución hay que tener especial cuidado ya que como no se realiza ninguna validación al emitir etiquetas de forma «cruda» el HTML generado podría estar mal balanceado.

Portada libro: PlugIn Tapestry

Libro PlugIn Tapestry

Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.


Variable not found: Desordenar un array en C# y VB.NET

$
0
0
Barajando el arrayHace unos días, el amigo Fernando J., me escribía a través del formulario de contacto del blog preguntándome si tenía a mano alguna solución para "desordenar" aleatoriamente elementos de un array en VB y, aunque no es el lenguaje ni el tipo de cuestión que solemos tocar por aquí, sí me pareció interesante darle una respuesta que pudiera ser de utilidad a alguien más.

Hay varios algoritmos para conseguirlo, pero el llamado Fisher-Yates shuffle es muy eficiente (O(N)), no necesita almacenamiento extra, es fácil de implementar y ofrece unos resultados más que razonables. Este algoritmo permite generar una permutación aleatoria de un conjunto finito de elementos o, en otras palabras, desordenar los elementos de un array.

El código Visual Basic .NET:
Sub Shuffle(Of T)(values As IList(Of T))
Dim n = values.Count()
Dim rnd = New Random()
Dim i = n - 1
While i > 0
Dim j = rnd.Next(0, i)
Dim temp = values(i)
values(i) = values(j)
values(j) = temp
i -= 1
End While
End Sub

' Usage:
Dim array = New Integer() {1, 2, 3, 4}
Shuffle(array)
' Content of array (example): { 2, 3, 4, 1 }
Sencillo, ¿eh? Lo único que hacemos es intercambiar el elemento N con uno elegido aleatoriamente entre los N-1 restantes, y repetir de nuevo para el elemento N-1, N-2, y así sucesivamente. De esta forma se asegura que todos los elementos habrán cambiado su posición durante el proceso; si no es esto lo que queremos y preferimos permitir que algunos elementos no cambien de lugar, simplemente habría que modificar el valor máximo del número aleatorio generado, de forma que existirá la posibilidad de que el elemento tratado quede finalmente en su misma ubicación: rnd.Next(0, i+1).

El equivalente al código anterior en C# sería el siguiente:
public static void Shuffle<T>(IList<T> values)
{
var n = values.Count;
var rnd = new Random();
for (int i = n - 1; i > 0; i--)
{
var j = rnd.Next(0, i);
var temp = values[i];
values[i] = values[j];
values[j] = temp;
}
}

// Usage:
var array = new[] {"a", "b", "c", "d"};
Shuffle(array);
// content of array (example): { "b", "d", "a", "c" }
Espero que pueda ser de ayuda :)

Publicado en Variable not found.

Variable not found: Enlaces interesantes 320

$
0
0
Enlaces interesantes
Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Otros

Publicado en Variable not found.

Poesía Binaria: Script para realizar capturas de pantalla rápidas con las opciones que necesito

$
0
0

Tanto en mi uso personal, para este blog, documentación, etc; como en el trabajo, la realización de capturas de pantalla forma parte de mi día a día. Y con el paso de los años he ido puliendo mi script de captura de pantalla para poder realizar las tareas que necesito de la forma más rápida. Aunque, con el tiempo han ido saliendo programas comoShutter o Flameshot, en ocasiones he tenido problemas con ellas. Por un lado, necesito que sean muy rápidas, para realizar múltiples capturas sin esperas; y por otro que ocupen poca memoria y sean estables, para que no se quede el ordenador congelado y no sucedan cosas raras.

De todas formas, desde hace mucho tiempo mantengo mi propio script para realizar capturas de pantalla, ayudándome de pequeños programas libres disponibles en la red. Esta necesidad surgió cuando el propio software que venía con el entorno de escritorio (Gnome, KDE, XFCE…) eran muy lentos para realizar una captura de pantalla, o, si precisaba seleccionar un área de la pantalla tenía que acceder a un modo gráfico, elegir una opción y repetir la captura. Actualmente ya no es así, aunque esos programas siguen tardando bastante tiempo en iniciar y es algo que me incomoda. Y, bueno, la principal razón por la que me encanta el software libre es que podemos personalizar hasta el más mínimo detalle.

Quien realiza de verdad la captura

Las capturas, al principio las hacía con import (de ImageMagick), más tarde me pasé a scrot. Se portaba algo mejor, era más compatible con diversas configuraciones y recuerdo que en la época en que me cambié había un bug en ImageMagick que dificultaba un poco mi trabajo. Scrot también tenía algunos problemas y finalmente estoy utilizando maim. Éste último es parecido a scrot, pero sin muchos de los problemas que tiene, incluso permite utilizar slop para realizar la selección de la zona a capturar. Todo queda muy bien y es muy bonito.

Otras cosas que necesito cuando capturo pantalla

Me gusta tener varias teclas rápidas para realizar capturas de pantalla, ya sean de la pantalla completa o de una región. Además:

  • Quiero guardar la captura automáticamente en disco.
  • Quiero que ese archivo aparezca en mis documentos recientes. Para subirla directamente al blog.
  • Quiero poder editarla con algún programa sencillo de retoque. O realizar acciones rápidas sobre las capturas.
  • Me gusta tener notificaciones en pantalla para saber que todo está marchando bien.
  • Necesito poder llamar scripts automáticamente cuando se realicen las capturas (eventos).
  • Crear miniaturas automáticamente. Es algo de agradecer cuando vas con las prisas. Es algo que no se tarda nada, pero si ya las tienes hechas, mejor.
  • A ser posible, que se puedan configurar y me lo pueda llevar a otro ordenador con pocos recursos

Además, para hacer el script lo más completo posible vamos a utilizar técnicas explicadas en otras publicaciones anteriores:

Dependencias

Aunque intento que solo requiera lo básico, maim para capturar, ImageMagick para el tratamiento, notify-send para recibir notificaciones de escritorio y poco más. Al final, utiliza zenity para mostrar interfaces gráficos con opciones (aunque cuando la cosa se complica, tiro de python y pygtk), recents para insertar la captura en documentos recientes y zbar (aunque no es requerido) para procesar códigos QR si es que la captura tiene alguno.

Para el entorno gráfico me gustaría probar con yad para ver si cumple mis requerimientos. Aunque también podría eliminar la dependencia de zenity y tirar de python para todo lo relacionado con el entorno gráfico.

Cosas por hacer

Tengo varias ideas como hacer un periodo de espera de unos segundos tras la captura en el que podemos pulsar una tecla para acceder a opciones sobre esa captura o la opción para tener una configuración temporal, que dure unos minutos, por si queremos cambiar el tiempo de disparo gráficamente. Querría organizar un poco más el código, poner las funciones un poco más ordenadas y documentar muchas de las cosas que vemos en el código. También estoy abierto a sugerencias, así que, si veis algo que os gustaría tener en el sistema de capturas de pantalla, podéis dejarlo en los comentarios.

¡Manos a la obra!

Sin más dilación, pongo aquí el código fuente. Aunque podréis encontrar versiones actualizadas en Github.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
#!/bin/bash

# To-do
# Temporary configuration. Para delay y texto de comentarios del archivo. El archivo de configuracion temporal solo
# Estara vigente durante un tiempo (CONFIGURABLE)

# do_capture: separar en varias funciones
readonlyVERSION="0.5"
readonlyLOCKFILE="/tmp/gscreenshot_"$(whoami)"_"$(echo$DISPLAY|tr':''_')
readonlySCRIPTPATH="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
readonly DEBUG=1
LOCKFD=150
readonly MYPID="
$$"
readonly MYNAME="
$(basename"$0")"
MAIM_EXE=
CONVERT_EXE=
IDENTIFY_EXE=
MOGRIFY_EXE=
NOTIFY_EXE=
RECENTS_EXE=
ZENITY_EXE=
PYTHON_EXE=
PYTHON_GTK=
ZBAR_EXE=
declare -A DEFAULTCONFIG
DEFAULTCONFIG=(["
_default_FILENAME_PATTERN"]="%HOME%/Screenshot_%DATETIME%.png"
                             ["
_default_CAPTUREDIR"]="Captures"
                             ["
_default_DATETIME_FORMAT"]="%Y-%m-%d-%H%M%S"
                             ["
_default_DATE_FORMAT"]="%Y-%m-%d"
                             ["
_default_TIME_FORMAT"]="%H%M%S"
                             ["
_default_INCLUDE_CURSOR"]="no"
                             ["
_default_BORDER_WIDTH"]=2
                             ["
_default_BORDER_COLOR"]="1,0,0,0.8"
                             ["
_default_OTHER_OPTIONS"]=
                             ["
_default_DEFAULT_DELAY_SECS"]=
                             ["
_default_NOTIFY_GLYPH"]=
                             ["
_default_NOTIFY_BEOFORE"]=yes
                             ["
_default_NOTIFY_AFTER"]=yes
                             ["
_default_NOTIFY_BEFORE_DELAY"]=1000
                             ["
_default_NOTIFY_AFTER_DELAY"]=2000
                             )
readonly DEFAULTCONFIG
WRED='\033[0;31m'
WGREEN='\033[0;32m'
WMAGENTA='\033[0;35m'
WNC='\033[0m' # No Color

GREENOK="
${WGREEN}Ok${WNC}"
REDERR="
${WRED}Error${WNC}"

function util_filename()
{
  local FILE="
$1"
  FILE="
${FILE##*/}"
  [[ "
$2" ]] && echo "${FILE%.*}" || echo "${FILE%%.*}"
}

function util_fextension()
{
  local FILE="
$1"
  FILE="
${FILE##*/}"

  [[ "
$FILE" = *.* ]] && ( [[ "$2" = "1" ]] && echo "${FILE#*.}" || echo "${FILE##*.}")
}

function util_dirname()
{
  local PATH="
$1"

  [[ "
$PATH" = */* ]] && echo "${PATH%/*}"
}

function show_version() {
        echo "
Script para hacer capturas de pantalla. gscreenshot. Version $VERSION"
        echo "
Por Gaspar Fernández <gaspy@totaki.com>"
        echo "
https://gaspar.totaki.com/gscreenshot/"
}

function _debug() {
        if [ "
$DEBUG" ]; then
                echo -e "
${WMAGENTA} DEBUG: $@${WNC}">&2
        fi
}

function _error() {
        echo -e "
${WRED} ERROR: $@${WNC}">&2
}

function extract_delimiters() {
        local ORIGINAL="
$1"
        local LEFT="
$2"
        local RIGHT="
$([-n"$3"]&&echo"$3"||echo"$2")"

        RES=${ORIGINAL#*${LEFT}};
        RES=${RES%${RIGHT}*}

        echo "
$RES"
}

function extract_delimiters_c() {
        local ORIGINAL="
$1"
        local LEFT="
$2"
        local RIGHT="
$3"

        if [[ ${ORIGINAL:0:1} == "
$LEFT" ]]; then
                extract_delimiters "
$ORIGINAL""$LEFT""$RIGHT"
        else
                echo "
$ORIGINAL"
        fi
}

function help() {
        show_version

        echo
        echo "
Sintaxis:"
        echo "
  gscreenshot [opciones]"
        echo
        echo "
Las opciones son las siguientes:"
        echo "
--type=xx, -t xx : Indica el tipo de captura a realizar:"
        echo "
    screen  : Captura la pantalla completa."
        echo "
    window   : Captura la ventana activa."
        echo "
    user     : Crea una captura interactiva."
        echo "
--check, -c     : Comprueba las dependencias."
        echo "
--preview, -p   : Previsualiza la captura con el programa seleccionado"
        echo "
--help, -h      : Presenta esta pantalla de ayuda."
        echo "
--version, -v   : Versión del programa."
        echo
}

function panic() {
        local ERRSTR="
$2"
        local ERRCODE="
$1"

        echo "
Error: $ERRSTR($ERRCODE)"
        # Errors:
        # From 1 to 10: initialization errors
        # From 10 to 50: capture errors
        # From 50 to 100: GUI errors
        # From 100 to infinity: user errors
        if [ $ERRCODE -gt 100 ]; then
                help
        elif [ $ERRCODE -gt 50 ]; then
                gui_error "
$ERRSTR"
        elif [ $ERRCODE -ge 10 ]; then
                launch_event "
ON_ERROR""""""""$ERRSTR"
        fi
        exit $ERRCODE
}

function gui_error() {
        $ZENITY_EXE --error --title="
Error en gscreenshot" --text "$@"
}

function gui_progress() {
        local TEXT="
$1"

        $ZENITY_EXE --progress --title="
gscreenshot" --text="$TEXT" --auto-close --time-remaining
}

function launch_event() {
        local EVENT_NAME="
$1"
        shift
        local EVENT="
$(get_setting "$EVENT_NAME")"
        if [ "
$EVENT" ]; then
                EVENT=$(generate_command "$EVENT""$1""$2""$3""$4")
                _debug $EVENT
                env sh -s <<< ${EVENT}
        fi
}

function check_dependencies() {
        MAIM_EXE=$(which maim)
        IDENTIFY_EXE=$(which identify)
        CONVERT_EXE=$(which convert)
        MOGRIFY_EXE=$(which mogrify)
        NOTIFY_EXE=$(which notify-send)
        RECENTS_EXE=$(which recents)
        ZENITY_EXE=$(which zenity)
        PYTHON_EXE=$(which python)
        ZBAR_EXE=$(which zbarimg)
        # If no input parameters panic mode when some program is not found
        local PANICMODE=false
        if [ -z "
$1" ]; then PANICMODE=true; fi

        if [ ! -r "
$MAIM_EXE" ]; then
                if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta maim"; fi
        fi

        if [ ! -r "
$CONVERT_EXE" ]; then
                if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta ImageMagick"; fi
        fi

        if [ ! -r "
$MOGRIFY_EXE" ]; then
                if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta ImageMagick"; fi
        fi

        if [ ! -r "
$IDENTIFY_EXE" ]; then
                if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta ImageMagick"; fi
        fi

        if [ ! -r "
$NOTIFY_EXE" ]; then
                if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta notify_send"; fi
        fi

        if [ ! -r "
$RECENTS_EXE" ]; then
                # No panic with no recents
                # if "
$PANICMODE"; then panic 5 "No se cumplen las dependencias. Falta recents"; fi
                true
        fi

        if [ ! -r "
$ZENITY_EXE" ]; then
                if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta zenity"; fi
        fi

        if [ ! -r "
$ZBAR_EXE" ]; then
                # No panic for zbar
                # if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta zbar"; fi
                true
        fi

        if [ ! -r "
$PYTHON_EXE" ]; then
                # No panic with no python
                # if $PANICMODE; then panic 5 "
No se cumplen las dependencias. Falta python"; fi
                true
        else
                PYTHON_GTK=$($PYTHON_EXE<<EOF
try:
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk
    print('1')
except ImportError as er:
    print('ERROR')
EOF
                                         )
        fi
}

function check_sc_tpe() {
        local TYPE=$1
        local _VALID_TYPES=("
screen""window""user")

        if printf '%s\0' "
${_VALID_TYPES[@]}" | grep -Fqxz "$TYPE"; then
                echo "
ok"
                return 0;
        else
                return 1;
        fi
}

function lock() {
        local PID="
$(cat$LOCKFILE)"
        echo {LOCKFD}<>$LOCKFILE

        flock -n $LOCKFD
        local STATUS=$?
        if [ $STATUS = 0 ]; then
                # Escribimos nuestra PID
                echo $MYPID>&${LOCKFD}
                return 0
        else
                local PROCNAME="
$(cat/proc/$PID/comm2>/dev/null)"
                if [ "
$PROCNAME" != "$MYNAME" ]; then
                        echo "
Error, el proceso ejecutado no coincide con el que debe"
                        exit 1
                fi
                local FROMTIME=$(awk -v ticks="$(getconf CLK_TCK)"
-vepoch="$(date +%s)"'
                        NR==1 { now=$1; next }
                        END { printf "%9.0f", epoch - (now-($20/ticks)) }'
/proc/uptimeRS=')'/proc/$PID/stat|xargs-idate +"%d/%m/%Y %H:%M:%S"-d@{})
                echo"El proceso $PID ($PROCNAME) lleva abierto desde $FROMTIME"

                return1
        fi
}

function exit_error(){
                echo"Ya hay una instancia en ejecución. Saliendo"
                exit1
}

function read_ini_file(){
    shopt-p extglob &>/dev/null

    localCHANGE_EXTGLOB=$?
    if[$CHANGE_EXTGLOB = 1]; then
        shopt-s extglob
    fi
        localCONFIG=
        declare-A CONFIG
    localFILE="$1"
    # Nombre por defecto cuando no hay sección
    localCURRENT_SECTION="_default"

    localini="$(<$FILE)"

    # Quitamos los \r usados en la nueva línea en formato DOS
    ini=${ini//$'\r'/}
    # Convertimos a un array
    IFS=$'\n'&&ini=(${ini})
    # Borra espacios al principio y al final (trim)
    ini=(${ini[*]/#+([[:space:]])/})
    ini=(${ini[*]/%+([[:space:]])/})
    # Borra comentarios, con ; y con #
    ini=(${ini[*]//;*/})
    ini=(${ini[*]//\#*/})

    for l in${ini[*]}; do
        if[["$l" =~ ^\[(.*)\]$ ]]; then
            CURRENT_SECTION="${BASH_REMATCH[1]}"
        elif[["$l" =~ ^(.*)="(.*)"]]||[["$l" =~ ^([^=]*)=(.*)]]; then
            localKEY="${CURRENT_SECTION}_"${BASH_REMATCH[1]%%+([[:space:]])}
            localVALUE=${BASH_REMATCH[2]##+([[:space:]])}
            CONFIG[$KEY]="$VALUE"
        else
            false
        fi
    done

    if[$CHANGE_EXTGLOB = 1]; then
        shopt-u extglob
    fi

        # CONFIG is now global
        local_CONFIG=$(declare-p CONFIG)
        # declare -p CONFIG >&2
        # Extracting only the interesting part (to avoid using eval)
        _CONFIG="$(extract_delimiters "$_CONFIG""'")"
        # Undo escaping
        _CONFIG=${_CONFIG//\'\\'\'/\'}
        echo$_CONFIG
        #declare -p _CONFIG
        #readonly CONFIG

}

function get_config_file(){
        locallocations=("$HOME/.gscreenshot""$HOME/.local/etc/gscreenshot""/etc/gscreenshot"".gscreenshot")
        for loc in${locations[@]}; do
                if[-r"$loc"]; then
                        echo"$loc"
                        return
                fi
        done
}

function read_config(){
        localCFGFILE="$(get_config_file)"
        if[-z"$CFGFILE"]; then
                return1
        fi
        read_ini_file "$CFGFILE"
        return0
}

function get_formatted_date(){
        localFORMAT="$1"
        localNOCACHE="$2"                  # If empty, use current DATE/TIME

        if[-z"$NOCACHE"]&&[!"$_CACHED_DATE"]; then
                _CACHED_DATE="$(date "+%s")"
        fi
        if[-n"$FORMAT"]; then
                [-z"$NOCACHE"]&&date--date="@$_CACHED_DATE" +"$FORMAT"||date +"$FORMAT"
        fi
}

function get_setting(){
        localSETTING="$1"
        localNAMESPACE=$([$2]&&echo"$2"||echo"_default")
        localFILTER="$3"
        local VAL

        # _debug "$SETTING - ${MAINCONFIG[${NAMESPACE}_${SETTING}]} - ${DEFAULTCONFIG[${NAMESPACE}_${SETTING}]}"
        if["${MAINCONFIG[${NAMESPACE}_${SETTING}]}"]; then
                VAL="$(extract_delimiters_c "${MAINCONFIG[${NAMESPACE}_${SETTING}]}" '"')"
        else
                VAL="${DEFAULTCONFIG[${NAMESPACE}_${SETTING}]}"
        fi

        case"$FILTER"in
                "number")
                        # If it's not a number, back to the default value
                        ![["$VAL" =~ ^[0-9]+$ ]]&&(
                                _error "La configuración $SETTING debe ser numérica. (Valor actual: $VAL)"
                                VAL="${DEFAULTCONFIG[${NAMESPACE}_${SETTING}]}"
                                )
                        ;;
                *)
                        ;;
        esac
        echo"$VAL"
}

function get_boolean(){
        localARG="$1"

        case"${ARG,,}"in
                "yes"|"true"|"1"|"si"|"y"|"s")
                        echotrue
                        ;;
                *)
                        ;;
        esac
}

function generate_filename(){
        localFILENAME="$1"
        localRECURSIVE="$2"                # When calling in a recursive way we may not want some replacements filled
        local REPLACEMENTS

        # TODO. We could make it global someway to improve speed
        declare-AREPLACEMENTS=(["HOME"]="$HOME"
                                                         ["CAPTUREDIR"]="$([[ "$RECURSIVE" =~ '/CAPTUREDIR' ]] || generate_filename "$(get_setting CAPTUREDIR)""$RECURSIVE/CAPTUREDIR")"
                                                         ["
THUMBNAILDIR"]="$([["$RECURSIVE" =~ '/THUMBNAILDIR']]||(generate_filename "$(get_setting THUMBNAILDIR)""$RECURSIVE/THUMBNAILDIR"))"
                                                         ["
SCRIPTDIR"]="$SCRIPTPATH"
                                                         ["
DATETIME"]="$(get_formatted_date "$(get_setting DATETIME_FORMAT)")"
                                                         ["
DATE"]="$(get_formatted_date "$(get_setting DATE_FORMAT)")"
                                                         ["
TIME"]="$(get_formatted_date "$(get_setting TIME_FORMAT)")"
                                                        )

        for KEY in "
${!REPLACEMENTS[@]}"; do
                VALUE=${REPLACEMENTS["$KEY"]}
                FILENAME=${FILENAME//"%$KEY%"/"$VALUE"}
        done

        echo "
$FILENAME"
}

function generate_metadata() {
        local RAW="
$1"
        local RECURSIVE="
$2"                # When calling in a recursive way we may not want some replacements filled
        local MDREPS

        # TODO. We could make it global someway to improve speed
        declare -A MDREPS=(["
YEAR"]="$(get_formatted_date "%Y")"
                                             ["
USERNAME"]="$(whoami)"
                                             ["
DATETIME"]="$(get_formatted_date "$(get_setting DATETIME_FORMAT)")"
                                             ["
DATE"]="$(get_formatted_date "$(get_setting DATE_FORMAT)")"
                                             ["
TIME"]="$(get_formatted_date "$(get_setting TIME_FORMAT)")"
                                            )

        for KEY in "
${!MDREPS[@]}"; do
                VALUE=${MDREPS["$KEY"]}
                RAW=${RAW//"%$KEY%"/""$VALUE""}
        done

        echo "
$RAW"
}

function generate_command() {
        local RAW="
$1"
        local TYPE="
$2"
        local FILENAME="
$3"
        local DELAY="
$4"
        local MSG="
$5"

        local MDREPS

        declare -A MDREPS=(["
TYPE"]="$TYPE"
                                             ["
FILENAME"]="$FILENAME"
                                             ["
DELAY"]="$DELAY"
                                             ["
MSG"]="$MSG"
                                            )

        for KEY in "
${!MDREPS[@]}"; do
                VALUE=${MDREPS["$KEY"]}
                RAW=${RAW//"%$KEY%"/""$VALUE""}
        done

        echo "
$RAW"
}

function show_notification() {
        local TITLE="
$1"
        local CONTENT="
$2"
        local DELAY="
$3"
        local EXTRA="
$4"                        # More configuration for notifications
        local GLYPH="
$(get_setting NOTIFY_GLYPH)"
        local NOTIFY_ARGS=()

        if [ "
$GLYPH" ]; then
                NOTIFY_ARGS+=("
-i""$(generate_filename "$GLYPH")")
        fi
        if [ "
$DELAY" ]; then
                NOTIFY_ARGS+=("
-t""$DELAY")
        fi

        NOTIFY_ARGS+=("
$TITLE""$CONTENT")
        _debug "
${NOTIFY_ARGS[@]}"
        $NOTIFY_EXE"
${NOTIFY_ARGS[@]}"
}

function do_capture() {
        local TYPE="
$1"

#       _debug ${MAINCONFIG[@}]
        local FILENAME="
$(generate_filename "$(get_setting FILENAME_PATTERN)")"
        local MAIM_ARGS=()

        case "
$TYPE" in
                "
window")
                        MAIM_ARGS+=("
-i $(xdotool getactivewindow)")
                        ;;
                "
user")
                        MAIM_ARGS+=("
-s")
                        local BORDERCOLOR="
$(get_setting BORDER_COLOR)"
                        local BORDERWIDTH="
$(get_setting BORDER_WIDTH)"
                        if [ "
$BORDERCOLOR" ]; then
                                MAIM_ARGS+=("
-c$BORDERCOLOR")
                        fi
                        if [ "
$BORDERWIDTH" ]; then
                                MAIM_ARGS+=("
-b$BORDERWIDTH")
                        fi
                        ;;
                "
screen")
                        ;;
                *)
                        ;;
        esac

        lock || show_notification "
Captura de pantalla""El proceso de captura está bloqueado. Ya hay una captura en curso"

        if [ "
$(get_boolean "$(get_setting INCLUDE_CURSOR)")" ]; then
                MAIM_ARGS+=("
--showcursor")
        fi

        DELAY=$(get_setting DEFAULT_DELAY_SECS)
        if [ "
$DELAY" ]; then
                MAIM_ARGS+=("
-d$DELAY")
        fi
        #echo ${MAINCONFIG[@]}

        MAIM_ARGS+=($(get_setting OTHER_OPTIONS))
        MAIM_ARGS+=("
$FILENAME")

        [ "
$(get_boolean "$(get_setting NOTIFY_BEFORE)")" ] && show_notification "Captura de pantalla""Seleccione el área a capturar""$(get_setting NOTIFY_BEFORE_DELAY)"

        launch_event "
ON_BEFORE_CAPTURE""$TYPE""$FILENAME""$DELAY"
        $MAIM_EXE"
${MAIM_ARGS[@]}"

        if [ "
$?" -ne "0" ];
    then
                [ "
$(get_boolean "$(get_setting NOTIFY_AFTER)")" ] && show_notification "Error""Fallo al capturar""$(get_setting NOTIFY_AFTER_DELAY)"
                panic 10 "
No se ha realizado la captura"
    else
                [ "
$(get_boolean "$(get_setting NOTIFY_AFTER)")" ] && show_notification "Muy bien""Captura realizada con éxito""$(get_setting NOTIFY_AFTER_DELAY)"
    fi

        launch_event "
ON_AFTER_CAPTURE""$TYPE""$FILENAME""$DELAY"

        if [ ! -r "
$FILENAME" ]; then
                # No deberíamos NUNCA estar aquí. Pero, siempre pueden pasar cosas raras.
                panic 11 "
El archivo de captura no existe"
        fi
        local COMMENT="
$(get_setting APPEND_COMMENT)"
        if [ "
$COMMENT" ]; then
                COMMENT="
$(generate_metadata "$COMMENT")"
                $MOGRIFY_EXE -comment "
$COMMENT""$FILENAME"
        fi

        [ "
$(get_boolean "$(get_setting USE_RECENTS)")" ] && recents -qa "$FILENAME"

        if [ "
$(get_boolean "$(get_setting MAKE_THUMBNAIL)")" ]; then
                local _MIN_WIDTH="
$(get_setting MIN_IMAGE_WIDTH "" number)"
                local _MIN_HEIGHT="
$(get_setting MIN_IMAGE_HEIGHT "" number)"
                if [ -n "
$(identify -format"%w %h""$FILENAME"|awk"{ if (\$1 >$_MIN_WIDTH&& \$2 > $_MIN_HEIGHT) print "true"}")" ]; then
                        local THUMB_FILE="
$(generate_filename "$(get_setting THUMBNAIL_PATTERN)")"
                        local DIRECTORY="
$(dirname"$THUMB_FILE")"
                        [ -d "
$DIRECTORY" ] || mkdir -p "$DIRECTORY"

                        local THCOMMENT="
$(get_setting APPEND_COMMENT_THUMBNAILS)"
                        if [ "
$THCOMMENT" ]; then
                                THCOMMENT="
$(generate_metadata "$THCOMMENT")"
                                convert "
$FILENAME" -comment "$THCOMMENT" -resize "$(get_setting THUMBNAIL_SIZE)""$THUMB_FILE"
                        else
                                convert "
$FILENAME" -resize "$(get_setting THUMBNAIL_SIZE)""$THUMB_FILE"
                        fi
                        launch_event "
ON_CREATE_THUMBNAIL""$TYPE""$THUMB_FILE""$DELAY""$FILENAME"
                fi
        fi

        local SUMMARYFILE="
$(generate_filename "$(get_setting SUMMARY_FILE)")"
        if [ "
$SUMMARYFILE" ]; then
                [ -d "
$(dirname"$SUMMARYFILE")" ] || mkdir -p "$SUMMARYFILE"
                touch "
$SUMMARYFILE"
                [ -r "
$SUMMARYFILE" ] || _error "Cannot access Summary File: $SUMMARYFILE"
                local LASTSHOTS="
$(head-n $(($(get_setting MAX_ENTRIES number)-1))"$SUMMARYFILE")"
                echo -e "
$FILENAME\n$LASTSHOTS"> $SUMMARYFILE
        fi
}

function summary_file() {
        local _SUMMARYFILE="
$(get_setting SUMMARY_FILE)"
        [ "
$_SUMMARYFILE" ] || panic 56 "No hay fichero de registro de capturas"
        local SUMMARYFILE="
$(generate_filename "$_SUMMARYFILE")"
        echo "
$SUMMARYFILE"
}

function do_preview() {
        local SUMMARYFILE="
$(summary_file)"
        local EXTERNALPREVIEW="
$(get_setting PROGRAM_EXTERNAL_PREVIEW)"

        [ -n "
$EXTERNALPREVIEW" ] || panic 53 "No se ha especificado el programa para previsualizar"
        [ -r "
$SUMMARYFILE" ] || panic 51 "No se encuentra el índice de capturas"
        local LASTONE="
$(head-n1"$SUMMARYFILE")"
        [ -r "
$LASTONE" ] || panic 52 "No se encuentra el fichero de captura <b>$LASTONE</b>"
        local PROGRAMNAME="
$(basename$EXTERNALPREVIEW)"
        [ "
$(which"$PROGRAMNAME")" ] || panic 54 "No se encuentra el programa <b>$PROGRAMNAME</b> para previsualizar archivos"
        _debug "
$(generate_command "$EXTERNALPREVIEW""""$LASTONE")"
        env sh -s <<< "
$(generate_command "$EXTERNALPREVIEW""""$LASTONE")"
}

function cache_store() {
        local FILENAME="
$1"
        local _CACHEDIR="
$(get_setting CACHE_DIRECTORY)"
        local AMWIDTH="
$(get_setting AM_THUMBNAIL_WIDTH "" number)"
        local AMHEIGHT="
$(get_setting AM_THUMBNAIL_HEIGHT "" number)"
        [ "
$_CACHEDIR" ] || panic 55 "No se ha especificado el directorio de caché"
        local CACHEDIR="
$(generate_filename "$_CACHEDIR")"
        _debug $CACHEDIR
        [ -d "
$CACHEDIR" ] || mkdir p "$CACHEDIR"
        [ -r "
$FILENAME" ] || return
        local IMAGEFILENAME="
$(util_filename "$FILENAME" t)"
        local IMAGEFILEEXT="
$(util_fextension "$FILENAME")"
        local IMAGEDIR="
$(util_dirname "$FILENAME")"
        local CACHEFILE="
${CACHEDIR}/thumb_${IMAGEFILENAME}.jpg"
        _debug $AMWIDTH x $AMHEIGHT
        if [ -r "
$CACHEFILE" ] &&
                     [ -n "
$(identify -format"%w %h""$CACHEFILE"|awk"{ if (\$1 <=${AMWIDTH}&& \$2 <= ${AMHEIGHT}) print "true"}")" ]; then
                # Cache file is already created and it looks good
                echo $CACHEFILE
                return
        fi
        convert "
$FILENAME" -resize ${AMWIDTH}x${AMHEIGHT}\> "$CACHEFILE"

        echo $CACHEFILE
}

function join_by() {
        local IFS="
$1";
        shift;
        echo "
$*";
}

function do_qr_code() {
        local FILENAME="
$1"
        local QRDELETE="
$(get_boolean "$(get_setting QR_DELETE_IMAGE)")"
        local QRCONTENT
        QRCONTENT="
$($ZBAR_EXE"$FILENAME")"
        local STATUS=$?

        if [ $STATUS -eq 0 ]; then
                $ZENITY_EXE --info --title="
gscreenshot" --text="$QRCONTENT"
                [ "
$QRDELETE" ] && rm "$FILENAME"
        else
                $ZENITY_EXE --error --title="
gscreenshot" --text="No se encontró el código QR"
        fi
}

function do_image_menu() {
        local IMAGEFILE="
$1"
        local AMWIDTH="
$(get_setting AM_WIDTH "" number)"
        local AMHEIGHT="
$(get_setting AM_HEIGHT "" number)"
        [ "
$PYTHON_GTK" ] || panic 57 "No se ha instalado Python o PyGTK"

        _OPTIONS=("
quick-edit=Edición rápida"
                         "
edit=Editar")
        _OPTIONS+=("
qr-code=Buscar QR-code")
        _OPTIONS+=("
exit=Salir")

        OPTIONS="
$(join_by $'\n'"${_OPTIONS[@]}"|sed'{:a;N;s/\n/\\n/g;t a}')"

        local ACTION="
$($PYTHON_EXE<<EOF
# -*- coding: utf-8 -*-
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf

class PyMenu(Gtk.Window):
    def __init__(self, imageFile, width, height, options):
        super(PyMenu, self).__init__()
        self.zoom=1

        self.set_size_request(width, height)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("destroy", Gtk.main_quit)
        self.set_title("Vista previa de imagen")

        grid = Gtk.Grid();
        grid.set_column_spacing(20)
        grid.set_row_spacing(20)

        label = Gtk.Label(label="Texto introductorio")
        self.box = Gtk.ScrolledWindow()
        self.box.set_policy(Gtk.PolicyType.AUTOMATIC,
                       Gtk.PolicyType.AUTOMATIC)

        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(imageFile)
        self.image = Gtk.Image()
        self.image.set_from_pixbuf(self.pixbuf)
        self.connect("size-allocate", self.on_size_allocate)
        self.box.add(self.image)
        self.box.set_hexpand(True)
        self.box.set_vexpand(True)

        buttonsData=options.split('\n')
        buttonbox = Gtk.Box(spacing=6)

        for i in buttonsData:
            btpart=i.partition('=')
            btn=Gtk.Button(btpart[2].strip())
            btn.userdata=btpart[0].strip()
            btn.connect("clicked", self.on_button_clicked)
            buttonbox.pack_start(btn, True, True, 0)

        grid.add(label)
        grid.attach(self.box,0,1,1,1)
        grid.attach(buttonbox, 0, 2, 1, 1)

        self.add(grid)
        self.show_all()

        self.image.set_size_request(self.box.get_allocation().width, height=self.box.get_allocation().height)

    def on_size_allocate(self, obj, rect):
        k_pixbuf = float(self.pixbuf.props.height) / self.pixbuf.props.width
        # rect = self.box.get_allocation()
        k_rect = float(rect.height) / rect.width

        if k_pixbuf < k_rect:
              newWidth = rect.width
              newHeight = int(newWidth * k_pixbuf)
        else:
            newHeight = rect.height
            newWidth = int(newHeight / k_pixbuf)

        base_pixbuf = self.image.get_pixbuf()
        if base_pixbuf.props.height == newHeight and base_pixbuf.props.width == newWidth:
            return

        base_pixbuf = self.pixbuf.scale_simple(
            newWidth*self.zoom,
            newHeight*self.zoom,
            GdkPixbuf.InterpType.BILINEAR
        )

        self.image.set_from_pixbuf(base_pixbuf)

    def on_button_clicked(self, widget):
        print(widget.userdata)
        self.destroy()

PyMenu("${IMAGEFILE}",${AMWIDTH},${AMHEIGHT}, "${OPTIONS}")
Gtk.main()
EOF

        )"

        case "
$ACTION" in
                "
quick-edit")
                        do_invoke "
PROGRAM_QUICK_EDIT""$ACTION""$IMAGEFILE"
                        ;;
                "
edit")
                        do_invoke "
PROGRAM_EDIT""$ACTION""$IMAGEFILE"
                        ;;
                "
qr-code")
                        do_qr_code "
$IMAGEFILE"
                        ;;
                *)
                        ;;
        esac

}

function do_invoke() {
        local INVOCATION="
$1"
        local ACTION="
$2"
        local IMAGEFILE="
$3"

        local PROGRAM="
$(get_setting "$INVOCATION")"

        [ -n "
$PROGRAM" ] || panic 58 "No se ha especificado el programa externo para $ACTION"
        local PROGRAMNAME="
$(basename$PROGRAM)"
        [ "
$(which"$PROGRAMNAME")" ] || panic 59 "No se encuentra el programa <b>$PROGRAMNAME</b> para $ACTION"

        _debug "
$(generate_command "$PROGRAM""""$IMAGEFILE")"
        env sh -s <<< "
$(generate_command "$PROGRAM""""$IMAGEFILE")"

}

function do_actions() {
        local SUMMARYFILE="
$(summary_file)"
        local AMWIDTH="
$(get_setting AM_WIDTH "" number)"
        local AMHEIGHT="
$(get_setting AM_HEIGHT "" number)"

        local EXIT=
        while [ ! $EXIT ]; do
                IFS=$'\n\r' &&  local FILES=($(cat "$SUMMARYFILE")) && unset IFS
                local THUMBNAILS=()

                for (( fi=0; fi < ${#FILES[@]}; fi++)); do
                        local FILE=${FILES[fi]}
                        local THUMB=("
$(cache_store "$FILE")")
                        local RESOL
                        local FSIZE
                        _debug "
AKA ---$THUMB"

                        [ "
$THUMB" ] &&
                                RESOL="
$(identify -format"%wx%h""$FILE")"&&
                                FSIZE="
$(numfmt --to=iec-i --suffix=B --format="%.3f""$(stat -c%s "$FILE")")"&&
                                IFS=$'\n'&& THUMBNAILS+=("
$(echo-e"${THUMB}\t${FILE}\t${RESOL}\t${FSIZE}")") && unset IFS

                        echo $(((fi+1)*100/${#FILES[@]}))
                done > >(gui_progress "
Creando miniaturas...")

                local OPTIONS=()
                IFS=$'\n' && for THF in ${THUMBNAILS[@]};do
                        IFS=$'\t' && for t in $THF; do
                                local THUMB="
${t[0]}"
                                local FILE="
${t[1]}"
                                local DIMS="
${t[2]}"
                                local SIZE="
${t[3]}"
                                OPTIONS+=("
$THUMB")
                                OPTIONS+=("
$FILE")
                                OPTIONS+=("
$DIMS")
                                OPTIONS+=("
$SIZE")
                        done
                done
                OUTPUT=$($ZENITY_EXE --list --width $AMWIDTH --height $AMHEIGHT\
                                        --text "
Elige uno"\
                                        --separator=$'\t' \
                                        --imagelist \
                                        --print-column 2 \
                                        --ok-label="
Vista previa"\
                                        --cancel-label="
Salir"\
                                        --column "
Vista previa"\
                                        --column "
Nombre del archivo"\
                                        --column "
Dimensiones"\
                                        --column "
Tamaño"${OPTIONS[@]})
                local STATUS=$?
                if [ "
$STATUS" -eq 0 ]; then
                        # Vista previa y mas
                        do_image_menu "
$OUTPUT"
                else
                        EXIT=1
                fi
        done
        # (
        #       for (( i=0; i<100; i++ )); do
        #               sleep 0.1
        #               echo $i
        #       done
        # ) | gui_progress "
Creando miniaturas..."
}

function cond_write() {
        local COND="
$1"
        local WRITEOK="
$2"
        local WRITERR="
$3"

        if [ "
$COND" ]; then
                echo -en "
$WRITEOK";
        else
                echo -en "
$WRITERR";
        fi
}

function do_dependency_check() {
        show_version
        echo
        check_dependencies 1
        echo -en "
Maim: "
        cond_write "
$([-n"$MAIM_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Convert: "
        cond_write "
$([-n"$CONVERT_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Identify: "
        cond_write "
$([-n"$IDENTIFY_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Mogrify: "
        cond_write "
$([-n"$MOGRIFY_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Notify-send: "
        cond_write "
$([-n"$NOTIFY_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Recents (no obligatorio): "
        cond_write "
$([-n"$RECENTS_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Zenity: "
        cond_write "
$([-n"$ZENITY_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Zbar: "
        cond_write "
$([-n"$ZBAR_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Python: "
        cond_write "
$([-n"$PYTHON_EXE"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
        echo -en "
Gtk for Python: "
        cond_write "
$([-n"$PYTHON_GTK"]&&echo1)""[${GREENOK}]\n""[${REDERR}]\n"
}

ARGS=$(getopt -q -o "t:aphvc" -l "type:,actions,preview,version,help,check" -n "args" -- "$@");
if [ $? -ne 0 ];
then
    echo "
Ha habido un error al procesar los argumentos."
    exit 1
fi

eval set -- "
$ARGS";

JOB=
TYPE=

while [ $# -gt 0 ]; do
        case "
$1" in
                -t|--type)
                        if [ -z "
$(check_sc_tpe $2)" ]; then
                                panic 1 "
Tipo de captura incorrecto. Vea la ayuda"
                        fi
                        JOB="
capture"
                        TYPE="
$2"
                        shift;
                        ;;
                -p|--preview)
                        JOB="
preview"
                        shift;
                        ;;
                -a|--actions)
                        JOB="
actions"
                        shift;
                        ;;
                -v|--version)
                        show_version
                        exit 0
                        ;;
                -c|--check)
                        do_dependency_check
                        exit 0
                        ;;
                -h|--help)
                        help
                        exit 0
                        ;;
                --)
                        shift;
                        break;
                        ;;
        esac
        shift
done

check_dependencies

if [ "
$JOB" ]; then
        if ! _MAINCONFIG=$(read_config); then
                echo "
Puede crear un fichero de configuración para establecer algunos parámetros comunes."
                echo
        fi
        declare -A MAINCONFIG="
${_MAINCONFIG[@]}"
        # echo ${MAINCONFIG[@]}
fi

case "
$JOB" in
        "
capture")
                do_capture "
$TYPE"
                ;;
        "
preview")
                do_preview
                ;;
        "
actions")
                do_actions
                ;;
        *)
                panic 2 "
Trabajo desconocido"
                ;;
esac

Configuración

El script require configuración. Y, para ello, he acudido a archivos tipo INI, y así evitar que alguien pueda tocar parte de la funcionalidad del script insertando código en el archivo de configuración (solo en los eventos podríamos). Normalmente, tocando solo el fichero de configuración podremos personalizar bastante el comportamiento del script. Dicho archivo podemos situarlo en (por orden de preferencia):

  • $HOME/.gscreenshot
  • $HOME/.local/etc/gscreenshot
  • /etc/gscreenshot
  • ./.gscreenshot

Uso

Aunque podemos utilizar varias opciones más, y el programa dispone de una pequeña ayuda para ello, las opciones más básicas son:

Verificar dependencias

Con esto, podemos saber si el programa va a funcionar bien, se comprueba que existan los programas que requiere para su correcto funcionamiento. Algunos serán requeridos y otros no.

gscreenshot -c
Script para hacer capturas de pantalla. gscreenshot. Version 0.5
Por Gaspar Fernández
https://gaspar.totaki.com/gscreenshot/
Maim: [Ok]
Convert: [Ok]
Identify: [Ok]
Mogrify: [Ok]
Notify-send: [Ok]
Recents (no obligatorio): [Ok]
Zenity: [Ok]
Zbar: [Ok]
Python: [Ok]
Gtk for Python: [Ok]

Captura de pantalla

Lo más importante, capturar la pantalla. Las capturas pueden ser de tres tipos:

  • screen: Captura la pantalla entera.
  • window: Captura la ventana actual.
  • user: Permite al usuario seleccionar la región a capturar.
gscreenshot -t [tipo]

Vista previa con programa externo

Previsualiza la última captura de pantalla con un programa externo. Yo uso geeqie para estas cosas que además me permite navegar un poco por las demás imágenes.

gscreenshot -p

Trabajo de forma gráfica con las capturas

Esto nos permite ver miniaturas de las capturas y realizar acciones sobre ellas como editarlas con diversos programas, o mirar si tienen un código QR y analizar su contenido. Podremos añadir más opciones como subir la captura a un servidor externo o cualquier cosa que se nos ocurra hacer con ellas. Para este menú se crean más miniaturas (más pequeñas que las primeras que se crean con el programa, para que encajen, aunque en el futuro se podrá utilizar otro sistema para crear el menú). Las miniaturas generadas aquí se guardarán en un directorio de caché para evitar generarlas todo el rato cada vez que se muestra el menú.

gscreenshot -a

Foto principal: unsplash-logoIgor Miske

The post Script para realizar capturas de pantalla rápidas con las opciones que necesito appeared first on Poesía Binaria.

Variable not found: Usar Razor desde una aplicación de consola .NET Core (1 de 2)

$
0
0
ASP.NET CoreHace poco un alumno de mi curso de ASP.NET Core MVC en CampusMVP me preguntaba sobre las posibilidades de utilizar Razor desde una aplicación de consola con el fin de aprovechar dicha sintaxis a la hora de componer emails. Ya en un artículo anterior vimos cómo podíamos conseguirlo desde una aplicación ASP.NET Core MVC, pero en este caso lo que vamos a ver es cómo conseguirlo desde fuera de ASP.NET Core, es decir, desde una aplicación de consola pura sin apenas dependencias a dicho framework.

El problema que tiene intentar usar Razor de esta forma es que estamos muy malacostumbrados ;) ASP.NET Core hace mucho trabajo por nosotros y puede hacernos ver que renderizar una vista es algo trivial, pero no lo es; la vista debe ser parseada para obtener de ella un código C# que más adelante será compilado al vuelo para generar un ensamblado que será anexado a nuestra aplicación de forma dinámica y que será utilizado en cada renderización. Y todo ello, de forma rápida y eficiente en recursos.

La renderización de una vista Razor desde una aplicación de consola “pura” consiste en seguir estos mismos pasos, pero de forma manual. Lo que veremos a lo largo de un par de posts es:
  • Cómo generar código C# parseando una plantilla Razor, es decir, un archivo .cshtml.
  • Cómo compilar el código C# obtenido y generar un ensamblado con Roslyn.
  • Cómo cargar dinámicamente dicho ensamblado en memoria.
  • Cómo ejecutar una vista presente en dicho ensamblado y obtener el resultado.
¡Empecemos! ;)
Nota: el objetivo de estos posts es puramente didáctico, y su única intención es aprender algo sobre las tripas de ASP.NET Core. No nos meteremos en optimizar estas operaciones o introducir mejoras como cacheado o similares, ni en ofrecer una solución funcionalmente completa. Por tanto, lo que veremos aquí no será production ready, pero sí un buen punto de partida para que podáis crear vuestras propias soluciones.

Cómo generar código fuente C# desde una plantilla Razor

Como hemos comentado antes, la primera fase de la ejecución de una plantilla Razor es “compilarla” a C#, es decir, parsearla y obtener de ella el código C# cuya ejecución generará el resultado final, listo para ser utilizado como cuerpo de un mail o para cualquier otro uso.

Afortunadamente, el framework ya nos proporciona herramientas para conseguirlo de forma bastante sencilla en el interior del paquete NuGet Microsoft.AspNetCore.Razor.Language, que debemos incluir nuestro proyecto. De hecho, este será el único paquete relacionado con ASP.NET Core que necesitaremos añadir a la aplicación.

En su interior encontraremos componentes capaces de leer un archivo .cshtml y generar clases C#, que tendrán siempre la siguiente estructura:
publicclass Template : [BaseClass]
{
publicasyncoverride Task ExecuteAsync()
{
...
}
}
Por defecto el nombre de la clase generada será Template, y la clase base de la que hereda, como veremos más adelante, es un dato que pasaremos al generador de Razor cuando vayamos a utilizarlo.

Ese método ExecuteAsync() que se incluye en las clases generadas es el encargado de escribir, ya en tiempo de ejecución, el resultado de la renderización de la plantilla mediante llamadas a métodos como WriteLiteral() y Write(). Para que quede más claro, a continuación mostramos un ejemplo de contenido de archivo Razor .cshtml:
Multiplication table of @Model

@for (int i = 1; i <=10; i++)
{
@:@Modelx @i = @(Model*i)
}
Observad que en este caso no es necesario utilizar la clásica directiva @model de Razor en ASP.NET Core, simplemente accedemos a la propiedad Model de la plantilla. Más adelante veremos de dónde sale esta propiedad.
La “compilación” de este archivo Razor generaría una clase con el siguiente método ExecuteAsync():
publicasyncoverride Task ExecuteAsync()
{
WriteLiteral("Multiplication table of ");
Write(Model);
WriteLiteral("\r\n\r\n");
for (int i = 1; i <= 10; i++)
{
WriteLiteral("");
Write(Model);
WriteLiteral(" * ");
Write(i);
WriteLiteral(" = ");
Write(Model*i);
WriteLiteral("\r\n");
}
}
El método WriteLiteral() se utiliza para escribir constantes de cadena, mientras que Write() se usa para escribir el contenido de expresiones Razor (las que comienzan con “@”). Pero sobre todo, fijaos en un detalle: ninguno de estos métodos está implementado en la clase generada automáticamente.

Por este motivo, a la hora de generar el código C# debemos indicar una clase base que será la encargada de proporcionar la implementación de estos métodos. Esta clase, además, puede incluir otros miembros interesantes, como por ejemplo una propiedad Model para que podamos suministrar a la plantilla información, al igual que hacemos con los view models de MVC.

Veamos una posible implementación de esta clase base, en la que introducimos un parámetro genérico TModel para usarlo como tipo de la propiedad Model:
publicclass TemplateBase<TModel>
{
private TextWriter _output = Console.Out;
protected TModel Model { get; set; }

protectedvoidWriteLiteral(string literal) => _output.Write(literal);
protectedvoidWrite(object obj) => _output.Write(obj);

public Task ExecuteAsync(TModel model, TextWriter output = null)
{
Model = model;
_output = output ?? _output;
returnthis.ExecuteAsync();
}
// Will be overriden by the template class
publicvirtual Task ExecuteAsync() => Task.CompletedTask;
}
Fijaos que hacemos que los métodos WriteLiteral() y Write() escriban sus argumentos sobre el TextWriter almacenado en la instancia. Dicho objeto es inicializado con el valor Console.Out, de forma que por defecto el resultado de ejecutar la plantilla será escrito sobre la consola, aunque podemos establecer una salida distinta al invocar a la sobrecarga apropiada de ExecuteAsync(), de forma que sea el proceso que renderice la plantilla el que decida hacia dónde escribir el resultado.

También al invocar el método ExecuteAsync() estableceremos el valor de la propiedad Model que usaremos para enviar información a la plantilla. Dado que la clase generada por Razor heredará TemplateBase<TModel>, podrá utilizar la propiedad Model sin problema.

Con esto, ya tenemos todas las piezas necesarias para utilizar el generador de Razor y obtener el código C# a partir de nuestra plantilla. Vamos a crear ahora una clase que gestione la generación de código desde la plantilla .cshtml, a la que llamaremos RazorGenerator, y cuya implementación veremos a continuación.

Su constructor recibe como argumento la ruta donde se encuentran los archivos Razor a procesar, relativa a la carpeta actual (normalmente, el directorio de bin desde donde se está ejecutando nuestra aplicación). Su único método, GenerateCode(), configura Razor para que genere las clases tal y como nos interesa, obtiene la plantilla solicitada y genera su código C# utilizando el motor de plantillas, retornándolo en forma de cadena de caracteres:
publicclass RazorGenerator
{
privatereadonlystring _templateRoot;

publicRazorGenerator(string templateRoot)
{
_templateRoot = templateRoot;
}
publicstring GenerateCode<TModel>(string templateName)
{
var engine = RazorEngine.Create(b =>
{
// Define the namespace for the generated class
b.SetNamespace($"{typeof(TemplateBase<>).Namespace}.Templates");
b.ConfigureClass((razorCode, outputClass) =>
{
// Define the generated class name
// TODO: Sanitize & ensure that templateName is a valid C# class name
outputClass.ClassName = templateName;
// Define its base type
outputClass.BaseType =
$"{typeof(TemplateBase<>).Namespace}."
+ nameof(TemplateBase<TModel>)
+ $"<{typeof(TModel).FullName}>";
});
b.Build();
});

var project = RazorProject.Create(
Path.Combine(Directory.GetCurrentDirectory(), _templateRoot)
);

var templateEngine = new RazorTemplateEngine(engine, project);
templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";

var item = project.GetItem(templateName + ".cshtml");
var code = templateEngine.GenerateCode(item);
return code.GeneratedCode;
}
}
// TODO: por simplificar, en este momento asumimos que el nombre de la plantilla siempre será un identificador de clase válido en C#, pero deberíamos comprobarlo y sanearlo antes de continuar.
Para utilizar el código anterior e ir viendo resultados, podríamos hacer algo parecido a lo que mostramos a continuación, donde compilamos la plantilla Templates/Hello.cshtml y mostramos el resultado por consola:
var generator = new RazorGenerator("Templates");    // Folder "Templates"
var code = generator.GenerateCode<string>("Hello"); // Template "Hello.cshtml"
Console.WriteLine(code);
Explorador de solucionesOjo, porque la ubicación de la plantilla es por defecto relativa al binario de la aplicación, por lo que lo más cómodo es acceder a las propiedades del archivo en el explorador de soluciones de Visual Studio y establecer la propiedad “Copy to Output Directory” a “Copy if newer”.

La plantilla Templates/Hello.cshtml podría tener un contenido como el siguiente:
@* Templates/Hello.cshtml *@
Hello, @Model
De esta forma, por consola obtendríamos el siguiente resultado: un flamante código C# listo para ser compilado de forma dinámica más adelante (lo he limpiado un poco por claridad):
namespace RazorRenderer
{
publicclass Test : TemplateBase<string>
{
publicasyncoverride Task ExecuteAsync()
{
WriteLiteral("Hello, ");
Write(Model);
}
}
}
¿Mola, eh?

Bueno, pues de momento lo vamos a dejar aquí. En el próximo post veremos cómo compilar este código y ejecutarlo para renderizar nuestra plantilla :)

Publicado en Variable not found.

Navegapolis: Empresas "teal", holocracia, sociocracia, y otras hierbas.

$
0
0

tertuliaDatos de 2014 de empresas de todo el mundo revelan que el 63% de los trabajadores no están motivados, y del 37% restante la mayoría son infelices. Es lo que afirma Ana Moreno en el vídeo incluido en este artículo(1), en el que señala que el problema afecta tanto a los trabajadores como a las empresas, porque implica una escasa capacidad para atraer y retener talento con competencias críticas.

En ese sentido, Frédéric Laloux, apunta que la principal causa del problema está en la organización tradicional de las empresas, basada en estructuras directivas piramidales, y objetivos diseñados con planes estratégicos. Un modelo que considera tóxico en sí mismo, y que produce organizaciones de vistosos edificios de mármol y vidrio, pero sin alma.

Como solución a estos problemas están surgiendo modelos de organización empresarial que, según sus defensores suponen un avance evolutivo que propone alternativas a las estructuras de gobernanza piramidal, a la planificación de estrategias predictivas (planes estratégicos), y al trabajo motivado por recompensas extrínsecas. Proponen como antítesis: autogestión frente a estructuras directivas piramidales, propósito evolutivo frente a visión planificada y autoplenitud en el trabajo frente a gestión de "recursos" humanos.

Los defensores de modelos basados en equipos autogestionados argumentan que en niveles de complejidad como los que manejamos actualmente, los sistemas de planificación, control y toma de decisiones por jerarquía no funcionan. Que los sistemas realmente complejos no tienen una planificación centralizada, y mencionan como ejemplos referentes los sistemas de la naturaleza basados en estructuras fractales, o los seres vivos.

Los detractores, que seguramente son mayoría en los niveles directivos de las empresas actuales, argumentan con similar vehemencia que no es ningún avance evolutivo, sino una moda más o menos peligrosa en la medida en la que resulta atractiva e ilusionante. Una de las críticas referentes, tanto por la solidez de la exposición, como por la relevancia de su autor: Steve Denning, es el artículo "Making Sense Of Zappos And Holocracy", que cuestiona las bondades del modelo Holacracy. Un modelo sociocrático desarrollado y registrado por el empresario Brian Roberston.

 

teal

 

Posiblemente tenga razón Frédéric Laloux al afirmar "Si crees que se necesita una jerarquía, no lo vas a entender" ;-)

 


Variable not found: 12 añazos

$
0
0
12 años!!Pues sí, amigos, como siempre por estas fechas, hago la misma reflexión: ni en la más optimista de mis previsiones habría pensado que este viaje duraría tanto. ¡Doce añazos ya!

Mucho han cambiado las cosas desde 2006. Por aquellos tiempos, Windows XP campaba a sus anchas en los equipos de escritorio. Usábamos Visual Studio 2005 para programar aplicaciones .NET Framework 2.0, usando un C# en el que se acababan de incorporar características novedosas como los tipos genéricos, nullables, o los data tables. No teníamos smartphones, simplemente teléfonos móviles. Han cambiado muchas cosas, pero una sigue intacta: las ganas de seguir aprendiendo y compartir por aquí lo que pienso que puede ayudar a otros.

Mil y pico entradas después de aquél mayo de 2006, y tras recibir a más de dos millones de visitantes, no puedo más que seguir agradeciéndoos el apoyo que me habéis dado desde entonces con vuestras visitas, vuestros comentarios y sugerencias para mejorar. Muchas gracias a todos.

El blog: cómo fue el año

En general, creo que seguimos gozando de buena salud :)

Obviamente ya quedaron atrás los tiempos en los que cada año se producían incrementos espectaculares en el número de visitas, e incluso en los últimos meses he notado un leve descenso en las gráficas, supongo que de alguna forma influenciado por el hecho de que hasta hace poco el blog no estuviera disponible el HTTPS (y esto estaba penalizado por Google).

Evolución de visitas 2006-2018

Durante el pasado año llegaron aquí 130.000 usuarios distintos, visitando un total de 225.000 páginas con un promedio de estancia de 4 minutos. Los amigos de Facebook suman 925, y a través de Twitter nos siguen ya cerca de 2.200 usuarios.

Como suele ser habitual, más del 80% de los visitantes sois hombres, aunque se ha producido un aumento del 2% de mujeres en este año respecto al año pasado, donde ya registramos un incremento del 1% sobre el anterior. Quizás sea un leve indicio de que están cambiando las cosas en este sentido.

Según Google, la mayoría de vosotros os encontráis en el rango de edad entre 25 y 34 años, y como era previsible, amantes de la tecnología, informática, internet y comunicaciones.
Visitas por navegador Chrome es el navegador más utilizado (77%), seguido muy de lejos por Firefox (12%). El uso de ambos ha crecido ligeramente respecto a años anteriores a costa de Edge, Opera y Safari.

Como también suele ser habitual, más del 80% de los visitantes llegan al blog a través de Google, y el tráfico directo llega al 10%. El resto se reparte entre referrals y accesos desde redes sociales.

Tras varios años observándose un incremento de visitantes procedentes de México, este año han superado en número a los de España, alcanzando respectivamente un 23% t 19% sobre el total. Tras ellos, Colombia se mantiene en un 10% del total de visitantes, Perú con el 8%, Argentina con un 6.5%, Chile (6%), y otros ya por debajo del 5% como Ecuador, Estados Unidos, Venezuela o Costa Rica.

Resumidamente, Variable not found continúa en su línea de ser un blog humilde y sin grandes pretensiones en cuanto a visitas o ingresos (os reiríais de lo que aporta AdSense a la causa :D). La única pretensión es que aprendamos juntos, y la mejor recompensa es teneros por aquí de vez en cuando.

De nuevo, muchas gracias a todos por vuestro apoyo y espero que podamos seguir, muchos años más, buscando juntos esta escurridiza variable.

Publicado en Variable not found.

Poesía Binaria: Herramientas para trabajar en equipo sin perder el control ni la libertad

$
0
0

Trabajo en equipo

Hace unos días, los amigos de Startgo Connection, publicaron un post con 7 herramientas para trabajar en equipo de forma freelance. Si bien es cierto que las comunicaciones cada día son más robustas, la evolución de HTML5, Javascript y las tecnologías que los rodean, así como los lenguajes de servidor y el hardware han evolucionado notablemente. El trabajo desde casa se vuelve cada vez más normal.

Atrás quedaron los tiempos en los que se debía enviar todo por e-mail y la comunicación era en modo texto. O aquellos en los que se utilizaban solo videoconferencias para hacer una llamada cara a cara. Actualmente, se puede realizar una gran parte del trabajo en equipo estando cada miembro en diferentes sitios del globo. Pero, como siempre, uno de los grandes problemas que encontramos es la ausencia de control y la ausencia de libertad en las comunicaciones.

Ausencia de control

Con este término, me refiero al hecho de que las comunicaciones o el almacenamiento de los datos, las realice una tercera persona o empresa. El hecho de que todos nuestros mensajes, tickets, tareas o comunicaciones pasen por los servidores de una empresa antes de llegar a su destino. Al mismo tiempo, el almacenamiento de datos se podría realizar en servidores de terceros ajenos a nosotros. Y eso tiene algunos problemas:

  • El servicio que nos ofrecen puede cambiar en cualquier momento.
  • Los dueños de la información no somos nosotros.
  • Nuestros datos están expuestos, ante cualquier problema en la empresa proveedora, podrían robar nuestros datos.
  • Puede que por razones contractuales, no podamos utilizar cierta información, por la ubicación de los servidores.

Ausencia de libertad

No podremos saber cómo funcionan estas aplicaciones, o si realmente funcionan como dicen que funcionan y nada más. Es decir, la empresa proveedora no está extrayendo más información de la necesaria, ni quedándose con datos que no les corresponden, y que, cuando borramos un dato, realmente se borra de sus servidores. Son cuestiones que puede que no nos preocupen a priori, pero tarde o temprano deberían importarnos.
Por otro lado, derivado del punto anterior. El proveedor puede echar el cierre y dejarnos sin servicio. Como gesto de buena voluntad, muchos suelen dejarte descargar cosas o dejarte hacer una transición a otro servicio, pero no siempre será así. Nadie nos puede decir que una gran empresa como las que proporcionan los servicios mencionados va a durar mucho tiempo.

Del mismo modo, nadie nos garantiza que un software libre dure para siempre y sea mantenido siempre, puesto que el tiempo de vida de las tecnologías es limitado. Pero, nosotros tendremos los datos en un formato legible que, incluso podemos conocer nosotros y nos facilitará la transición. Además, como parte la magia del software libre, cuando un proyecto se abandona, si éste sigue siendo útil, no hay que esperar mucho tiempo antes de que alguien haga un fork y cree un proyecto derivado.

Un punto en contra

Aunque soy defensor del software libre, y de tener los servicios que utilizo en mis servidores, en mis máquinas y bajo mi control. Todo esto tiene también un precio. Cuando un software te gusta y lo utilizas, y no te ha costado dinero, es una buena idea y moralmente necesaria, gratificar a los desarrolladores de ese proyecto de alguna forma. No tiene por qué ser económica. Como desarrollador, el simple hecho de recomendar mis posts, mis programas o mis guías a otras personas me resulta muy satisfactorio. Es cierto que si alguien realiza una donación sí que me hace más feliz, aunque cuanta más gente conozca el proyecto, mayor porcentaje de personas habrá que hagan una donación.

Otro punto en contra es el mantenimiento. Al tener los servicios instalados en tus servidores, y bajo tu control. Tú te tienes que encargar de las actualizaciones, del mantenimiento y la responsabilidad del almacenamiento y los ficheros es tuya. Eso sí, los servicios no estarán limitados, el número de usuarios depende de la infraestructura que montemos y no de nuestra suscripción y el tamaño de archivos podrá ser mucho mayor, ya que nosotros somos los encargados de contratar el espacio. Es más, si no queremos contratar nada, podemos tener un pequeño ordenador en casa o en la oficina que controle estos servicios. Por ejemplo estos, dan un gran rendimiento en poco espacio y tienen un bajo consumo eléctrico:

Punto de vista económico

Desde el punto de vista económico, nos tocará hacer cuentas para ver si nos compensa. ¿Cuánto vale el tiempo que invertimos en mantenimiento? ¿Cuánto vale una suscripción a un servicio? ¿Cuánto vale nuestra información? Habrá sistemas en los que no nos importe que nuestra información vuele por la nube libremente, incluso que el servicio sea gratis. En otras ocasiones, necesitaremos tener nuestra información controlada y no hay vuelta atrás. Pero en otros muchos casos, puede que el número de usuarios sea determinante. Es decir, una suscripción para 10 usuarios cuesta 200€/año, pero yo puedo montar el servicio en un servidor pagando la mitad o, incluso tener varios servicios extra. Es cuestión de evaluarlo.

Las herramientas recomendadas

La mayoría de estas herramientas permitirán la instalación de las mismas en un servidor o en un ordenador de nuestra propiedad. En ocasiones, no tiene que ser todo trabajo remoto, incluso puedes tener las aplicaciones instaladas en una red interna y que nadie pueda acceder desde fuera. Otras veces, los propios desarrolladores ganan dinero ofreciendo sus servicios instalados y usables en una máquina, ofreciendo servicios de suscripción como siempre.

Gestión de tareas Kanban (como Trello)

Aunque Trello es muy completo. No es la única herramienta de su especie. Podemos utilizar como alternativas Wekan, un desarrollo muy activo que está tomando muy buena pinta últimamente.


Otras alternativas que merece la pena ver son Kanboard y Taskboard. Encontramos otro Kanban con muy buena pinta dentro de Gitlab para la gestión de incidencias, pero podemos utilizarlo como mejor nos venga. Aunque, si eres muy friki, tal vez te puedas defender bien con org-mode de Emacs con muchísimas opciones.

Almacenamiento compartido (como Google Drive, Dropbox, etc)

Una de las claves del trabajo en equipo es poder compartir archivos rápidamente. Así como poder tener una copia de seguridad de tus elementos importantes en varios dispositivos, incluso poder editar ciertos archivos en cualquier lugar. Google Drive o Dropbox ofrecen soluciones para ello, aunque estaremos hablando de opciones privativas y, por supuesto, la información quedará controlada por ellos.

Podemos utilizar aplicaciones como OwnCloud o NextCloud. Estos programas nos permitirán compartir archivos entre miembros del equipo, incluso personas fuera de la organización con contraseña y caducidad del enlace. Nos permitirán gestionar calendarios compartidos, contactos, notas, enlaces, documentos y mucho más. Aunque la guía es algo antigua podemos echar un ojo a Guía para instalar OwnCloud en 2015. Instalación básica (I). Tendremos control total sobre los archivos de nuestra organización.
Podremos instalar OwnCloud en nuestro ordenador, un NAS, miniPC, o Raspberry PI, tener todo el espacio que le queramos enchufar, compramos un disco duro de 1Tb, 2Tb, ponemos varios discos duros en RAID, o contratamos espacio en Amazon S3 u otro sistema de almacenamiento por bloques de acuerdo a nuestras necesidades.
Además, con la libertad que nos da NextCloud podemos dejar de utilizar los servicios de Google o de Apple para nuestra información personal de nuestro teléfono.

También se merece una mención especial Minio

Chat/Comunicaciones en grupo

En el mundo empresarial se escucha mucho Slack, como una forma fácil de tener una comunicación constante con nuestro equipo de trabajo (o clientes) en tiempo real. El sistema utiliza una interfaz web y podemos utilizarlo tanto desde un navegador como desde un cliente de escritorio (hecho con Electron). Bien, Slack no es la única opción, por un lado, todo se almacena en los servidores de Slack, además, aunque tienen una versión gratuita, está muy limitada y a poco que crezca tu equipo de trabajo los mensajes almacenados no serán suficientes, querrás buscar conversaciones antiguas y no encontrarás nada porque se van borrando automáticamente y encontrarás cosas que no puedes hacer con ella. Como alternativas encontramos Mattermost (tienen un plan de pago en sus servidores, pero te dan el software para que lo instales en tu propio servidor y no tengas límites). Mattermost también forma parte de Gitlab (software del que hablaremos más adelante).
También tenemos una gran alternativa llamada Rocket Chat con muchas opciones y por supuesto, como Slack y Mattermost clientes de escritorio y móviles para las plataformas más importantes.

Gestión de código

Si hablamos de la gestión de código fuente, control de versiones, incidencias, errores, bugs, etc. Y, sobre todo, en un entorno colaborativo. Lo que más se oye es GitHub. Utiliza git, sistema que sí es libre, creado por Linus Torvalds. Aunque los datos se encuentran en servidores que no controlamos y todo el sistema web y gestión de GitHub no es libre. Como alternativa tenemos Gitlab. Que nos da un sistema completísimo para gestionar nuestro código con gran cantidad de utilidades para llevar el trabajo en equipo a otro nivel. Además, tenemos sistemas de integración y despliegue continuos, sistema de creación de páginas como GitHub Pages, un sistema de chat con nuestro equipo y mucho más.

Gestión de incidencias

Si queremos registrar, controlar y realizar un seguimiento de las incidencias que reportan nuestros clientes. Una de las plataformas más famosas es OSTicket. Incluso, sale en la primera temporada de la serie Mr. Robot. Podemos utilizarlo tanto para tickets internos, dentro de la misma empresa, o entre departamentos; o tickets externos, directamente con los clientes. De esta forma podemos ver todas las incidencias que se han generado y buscarles solución lo más pronto posible. También tiene un pequeño sistema de base de conocimiento donde podemos ir registrando las respuestas o los métodos más preguntados y no tendremos que redactar los mensajes completos para dar solución a los problemas.
Otro software que podemos utilizar es Bugzilla. Uno de los clásicos, pero que a más de uno nos ha salvado la vida.

Servicio de correo

Actualmente, muchas empresas o particulares utilizan servicios como Gmail o Outlook para gestionar su correo electrónico. Aunque el software utilizado no es libre y los mensajes no se alojan físicamente bajo nuestro control. Como alternativa, podemos utilizar Cyrus para gestionar el correo entrante, Postfix para gestionar el correo saliente y Roundcube si queremos un entorno web para gestionarlo todo. Podemos seguir esta guía para configurarlo todo. Todo esto podremos tenerlo, sin ningún problema con nuestro dominio propio. El servidor de correo no se recomienda tenerlo en una IP dinámica, por lo que deberemos contratar un VPS o similar para alojarlo. Además, de esta forma, tendremos tanto espacio como hayamos contratado, podremos tener backups en nuestro ordenador y tantos usuarios, alias y listas como queramos, cosa que se agradece cuando nuestra organización va creciendo.

Sitios web rápidos

¿Quieres tener una página lista en cuestión de minutos? Puedes utilizar Gitlab Pages o incluso un WordPress.org. Así toda la información quedará alojada en tus propios servidores. Incluso tendrás la libertad para escribir scripts que aumenten las funcionalidades de las páginas eliminando las restricciones que nos darían Google Sites o WordPress.com

Monitorización del tiempo

Muchos hablan de Toggl o Wakatime. Si buscas una alternativa libre en la que los datos se queden bajo tu control y utilicemos software libre. Debes probar ActivityWatch. Este software nos permite conocer en qué se nos va el tiempo de nuestros proyectos. O si al final del día hemos pasado más tiempo en Facebook que trabajando.

Foto principal: unsplash-logorawpixel

The post Herramientas para trabajar en equipo sin perder el control ni la libertad appeared first on Poesía Binaria.

Blog Bitix: Gestión de errores con Either en vez de con código de error, null, Optional, checked exception o unchecked exception

$
0
0

A lo largo del tiempo han surgido varias formas de gestionar las excepciones. En C hace muchos años eran con códigos de error, en Java se incorporaron en el lenguaje las excepciones checked o uncheked o la nueva clase Optional en Java cada una con sus ventajas y y algunas deficiencias. Más recientemente usando un tipo tal que Either son otra forma para el tratamiento de errores sobre las opciones anteriores.

Java

Una parte importante para el correcto funcionamiento de un programa corresponde a la gestión de errores que pueden producirse en su ejecución. Si se trata de un programa que se comunica vía interfaz de red ha de estar preparado ante la situación que la conexión se pierda o se produzcan errores en la transmisión porque por ejemplo se ha desconectado el cable de red o la WiFi no es estable. Si se trata de un programa que guarda datos en el almacenamiento persistente también pueden producirse errores como que el archivo ya existe, el directorio no existe o el espacio del disco se ha agotado. Los posibles casos de error que pueden producirse en un programa son muchos y variados.

En épocas más antiguas una forma de gestionar los errores era y sigue siendo con códigos de error donde la función o el código de salida de un programa retorna un 0 si no se ha producido ningún error o un número distinto de cero si se ha producido algún error, con un código de salida diferente por cada error. Dado que no hay obligación de gestionar adecuadamente el código de salida a veces no se hace con el consiguiente posible mal funcionamiento del programa. Otra forma de código de error es retornar un valor null en un método o función pero que no tratado adecuadamente producirá una excepción de tipo NullPointerExcetion. Con la introducción de la clase Optional entre otras novedades de Java 8 los punteros nulos se gestionan más adecuadamente pero en los casos en los que se devuelve un puntero null no se proporciona información de cuál ha sido la condición de error que se ha producido.

Para obligar a gestionar adecuadamente las condiciones de error e informar de que posibles condiciones de error se pueden producir se incorporaron en algunos lenguajes las excepciones como en Java. Las excepciones checked, aquellas que son declaradas y de obligado tratamiento, garantizan que sean tratadas de alguna forma pero algo molestas con las sentencias try-catch-exception. Las excepciones unchecked, aquellas que no es necesario declararlas y no de obligado tratamiento, son arriesgadas ya que al igual que los códigos de error no obliga a darles un tratamiento además de que no se declaran que excepciones es posible que sean lanzadas.

En algunos lenguajes con capacidades funcionales se ha propuesto una nueva forma para la gestión de condiciones de error, en Java y con la librería Vavr se proporciona la clase Either que es un tipo con la definición de tipo genérico Either<L,R>. Que un método devuelva Either indica que puede devolver en el caso del ejemplo un Integer en el caso correcto o una excepción en el caso de error. Un potencial fallo de esta opción es que no hay obligación de usar un try-catch pero si se quiere usar el valor devuelto en caso correcto se ha de tener en cuenta el potencial caso de que lo haya es valor derecho. La clase Either proporciona métodos para tratar adecuadamente en caso de que esté presente el valor izquierdo o el valor derecho.

La clase Either tiene múltiples métodos para comprobar si el valor que tiene es un valor del tipo izquierdo, derecho, obtener el valor izquierdo, derecho y múltiples métodos que hereda de Value.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

Blog Bitix: Gestión de errores con Either o Try en vez de con código de error, null, Optional, checked exception o unchecked exception

$
0
0

A lo largo del tiempo han surgido varias formas de gestionar las excepciones. En C hace muchos años eran con códigos de error, en Java se incorporaron en el lenguaje las excepciones checked o uncheked o la nueva clase Optional en Java cada una con sus ventajas y y algunas deficiencias. Más recientemente usando un tipo tal que Either son otra forma para el tratamiento de errores sobre las opciones anteriores.

Java

Una parte importante para el correcto funcionamiento de un programa corresponde a la gestión de errores que pueden producirse en su ejecución. Si se trata de un programa que se comunica vía interfaz de red ha de estar preparado ante la situación que la conexión se pierda o se produzcan errores en la transmisión porque por ejemplo se ha desconectado el cable de red o la WiFi no es estable. Si se trata de un programa que guarda datos en el almacenamiento persistente también pueden producirse errores como que el archivo ya existe, el directorio no existe o el espacio del disco se ha agotado. Los posibles casos de error que pueden producirse en un programa son muchos y variados.

En épocas más antiguas una forma de gestionar los errores era y sigue siendo con códigos de error donde la función o el código de salida de un programa retorna un 0 si no se ha producido ningún error o un número distinto de cero si se ha producido algún error, con un código de salida diferente por cada error. Dado que no hay obligación de gestionar adecuadamente el código de salida a veces no se hace con el consiguiente posible mal funcionamiento del programa. Otra forma de código de error es retornar un valor null en un método o función pero que no tratado adecuadamente producirá una excepción de tipo NullPointerExcetion. Con la introducción de la clase Optional entre otras novedades de Java 8 los punteros nulos se gestionan más adecuadamente pero en los casos en los que se devuelve un puntero null no se proporciona información de cuál ha sido la condición de error que se ha producido.

Para obligar a gestionar adecuadamente las condiciones de error e informar de que posibles condiciones de error se pueden producir se incorporaron en algunos lenguajes las excepciones como en Java. Las excepciones checked, aquellas que son declaradas y de obligado tratamiento, garantizan que sean tratadas de alguna forma pero algo molestas con las sentencias try-catch-exception. Las excepciones unchecked, aquellas que no es necesario declararlas y no de obligado tratamiento, son arriesgadas ya que al igual que los códigos de error no obliga a darles un tratamiento además de que no se declaran que excepciones es posible que sean lanzadas.

En algunos lenguajes con capacidades funcionales se ha propuesto una nueva forma para la gestión de condiciones de error, en Java y con la librería Vavr se proporciona la clase Either que es un tipo con la definición de tipo genérico Either<L,R>. Que un método devuelva Either indica que puede devolver en el caso del ejemplo un Integer en el caso correcto o una excepción en el caso de error. Un potencial fallo de esta opción es que no hay obligación de usar un try-catch pero si se quiere usar el valor devuelto en caso correcto se ha de tener en cuenta el potencial caso de que lo haya es valor derecho. La clase Either proporciona métodos para tratar adecuadamente en caso de que esté presente el valor izquierdo o el valor derecho.

La clase Either tiene múltiples métodos para comprobar si el valor que tiene es un valor del tipo izquierdo, derecho, obtener el valor izquierdo, derecho y múltiples métodos que hereda de Value.

En vez de retornar un Either en un método usando Vavr se puede utilizar la clase Try como otra forma de gestionar las excepciones. Con Try el método no es necesario que devuelva un Either de modo que retorne el valor en el caso correcto y lance una exepción en caso de error. El Try puede convertirse a un Either.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

Koalite: Ingenieros o artesanos

$
0
0

No es un tema novedoso, más bien al contrario. Pero es un tema entretenido que últimamente me he cruzado en varias conversaciones en twitter y me parece una ocasión como otra cualquiera para escribir sobre él: ¿los desarrolladores de sofware somos ingenieros o artesanos?

Vaya por delante que no pretendo repartir ni títulos de ingeniero ni pegatinas de crafter-clean-coder. De hecho, si algo comparten ambas visiones es cierto elitismo que no me gusta. También quiero dejar claro (aunque ya lo hice en su día) que no considero necesario ningún título para ejercer esta actividad, así que cuando hablo de ingeniería no me refiero tanto a unos estudios reglados sino en una forma de hacer las cosas.

Soy ingeniero

ingeniería: conjunto de conocimientos orientados a la invención y utilización de técnicas para el aprovechamiento de los recursos naturales o para la actividad industrial.

Esa es la primera acepción de ingeniería que aparece en el DRAE (énfasis mío). Cuando terminé la carrera de Ingeniería Superior Informática en el año 2001 y me convertí en ingeniero (bueno, más bien me dieron un título que ponía eso), mi visión del desarrollo de software cuadraba bastante bien con esa definición.

Entendía que el desarrollo de software era una actividad industrial y, como tal, podían aplicarse diferentes técnicas para gestionarla, controlarla y dirigirla. Mis conocimientos eran bastante escasos, pero asignaturas como Ingeniería del Software me habían enseñado (es un decir) que existían metodologías que podían ser aplicadas al proceso de desarrollo que permitirían convertirlo en algo parecido a una cadena de montaje.

Se podía partir todo el proceso en distintas fases, perfectamente delimitadas, en cada de una de las cuales entrarían en juego los roles apropiados para producir el resultado esperado. Así, tendríamos una fase de análisis de requisitos en la que analistas funcionales generarían un documento de especificación de requisitos, una fase de diseño en la que arquitectos y diseñadores mapearían esos requisitos a componentes, una fase de implementación en las que se programarían los requisitos o una fase de pruebas en las que se verificaría que la implementación era correcta.

Al tener todas estas fases, roles y entregables tan bien definidos, era fácil establecer la analogía con cadenas de montaje (esa actividad industrial a la que hace referencia la definición de la DRAE) y subestimar el componente humano de cada una. A fin de cuentas, picar código es picar código y cambiar un programador por otro no debería ser problema, o incluso subiendo el nivel de abstracción, el mismo razonamiento podía ser aplicado a analistas, arquitectos o diseñadores.

Cuando empecé a trabajar en el Mundo Real™, con proyectos reales, mis convicciones emperazon a tambalearse. Por mucho que intentara aplicar lo que (creía que) sabía sobre Ingeniería del Software, la realidad se empeñaba en no hacerlo funcionar. Las fases no se podían marcar tanto, los requisitos nunca llegaban a estar completamente definidos, había que saltar hacia delante y hacia atrás entre fases continuamente y, por supuesto, las personas eran clave. Si alguien conocía una parte del código o del negocio, no era fácilmente reemplazable. No había ningún tipo de magia que permitiera coger la documentación de un proyecto (generalmente incompleta y desactualizada) y hacer que alguien siguiera con él como si nada hubiera pasado.

Parecía que todo aquello que había aprendido no fuera aplicable al mundo real, o al menos a mi mundo real, y que en equipos pequeños todo esto de los procedimientos establecidos, las metodologías, los entregables y demás, no eran más que una pérdida de tiempo que no servía para gran cosa ante clientes con requisitos cambiantes y plazos agobiantes que impedían ponerse a generar y mantener documentación.

Me he sentido artesano

En ese escenario es comprensible que, cuando conocí una cosa llamada manifiesto ágil me resultara atractiva. De repente había encontrado gente que asumía mi realidad de cada día y, en lugar de intentar luchar contra ella, intentaba trabajar lo mejor posible con (o a pesar de) ella.

Ideas como valorar más los individuos que los procesos, el software que la documentación, la colaboración con el cliente que discutir eternamente documentos de requisitos, o la capacidad de adaptación frente a seguir un plan preestablecido resultaban prometedoras.

Por aquella época surgió el movimiento de ALT.NET que me ayudó a conocer otras formas de trabajar. Hay que tener en cuenta que hasta ese momento mis referencias sobre cómo había que hacer las cosas eran guías de empresas como Microsoft o Sun, siempre enfocadas al desarrollo enterprise (en el peor de los sentidos) y al demoware. Aprender sobre DI, TDD, integración continua, programación extrema o refactorización me llevó a un mundo en el que me sentía más cómodo. Esas sí eran herramientas que podía utilizar en mi día a día y a las que sí que les sacaba partido, no a esas teorías antediluvianas de desarrollo en cascada que había aprendido en la universidad.

De ahí a empezar a sentirme más cercano al movimiento de software craftsmanship había un paso. Sí, seguía siendo un ingeniero (tenía mi título), pero cada vez me veía más como un artesano:

artesano: persona que ejercita un arte u oficio meramente mecánico. Usado modernamente para referirse a quien hace por su cuenta objetos de uso doméstico imprimiéndoles un sello personal, a diferencia del obrero fabril.

La segunda acepción de la DRAE (enfásis mío) cuadra muy bien con lo que pensaba. Desarrollar software no era como trabajar en una cadena de montaje. No éramos obreros reemplazables en una fábrica (siempre me espantó -y lo sigue haciendo- el concepto de software factory), sino que imprimíamos nuestro sello personal.

Aunque siempre he valorado mucho la formación teórica que recibí en la carrera, esto me hizo también cuestionarme cuánto de lo que había aprendido en lo referente a desarrollo de software me servía para algo y si no sería mejor seguir esa línea de formación de la que hablaban algunos con sus aprendices y maestros, transmitiendo y compartiendo conocimento mientras trabajan juntos programando por parejas.

Todo esto estaba muy bien, pero al cabo del tiempo empecé a ver cosas que me encajaban menos.

Casi todo acaba justificándose porque “el desarrollo de software es una actividad muy compleja, que no se puede comparar a otras áreas como al ingeniería civil”. Eso hacía que, si por ejemplo, estimar era difícil, la solución propuesta fuera no estimar, o que se infravalorase el conocimiento formal a la hora de optimizar el rendimiento, o que se llegara a ignorar las características de herramientas complejas y potentes pensando que con un poco de pair programming y unos tests se podían obtener resultados equivalentes.

Y ahora…

Ahora no sabría muy bien como clasificarme, pero creo que vuelvo a acercarme más a la idea de desarrollo como ingeniería que como artesanía.

Ya no creo, como me pasaba hace 20 años, que sea posible desarrollar software como el que construye casas (cuánto daño ha hecho ese símil), y seguramente la industrialización del desarrollo de software no pase por establecer procesos mecánicos que permitan convertir a las personas en recursos intercambiables supeditados a esos procesos. Creo que hay un componente de libertad en el desarrollo que requiere cierto arte, talento, o como prefieras llamarlo, que está más allá de reglas preestablecidas. O al menos, que no es fácil enmarcar dentro de unas cuantas reglas que podamos seguir al pie de la letra.

Hay una parte del desarrollo que tal vez no sea ingeniería y esté más relacionada con comunicación, exploración de soluciones y habilidades menos formales. No sé que porcentaje del desarrollo es esa, pero hay otra parte que sí es ingeniería y deberíamos ser “más ingenieros”. Puede que no seamos capaces de convertir el desarrollo de software en fabricación de sillas, pero hay aspectos concretos que podemos tratar con un rigor similar al que usaría un arquitecto para elegir sus materiales.

Tenemos a nuestra disposición teoremas sobre sistemas distribuidos, formas de tratar con fallos, leyes sobre optimización, análisis de complejidad de algoritmos y un sinfín de herramientas rigurosas que podemos utilizar.

Como escribía hace poco Gonzalo, debemos huir de ese relativismo al que nos agarramos con tanta frecuencia. Sí, el contexto importa, pero eso no hace que todo valga. Fijado un contexto, podemos encontrar técnicas objetivamente mejores, y eso es lo que debemos hacer: fijar contextos y trabajar sobre ellos. No se trata de buscar balas de plata, sino de saber qué tipos de balas son útiles en cada caso, y no, no todas las balas sirven para todo.

La cuestión real

La cuestión de fondo, y eso es mucho más entretenido que mis vaivenes a lo largo de los años, es hasta dónde se puede llegar a formalizar el desarrollo de software como disciplina.

La visión clásica que trataba de equiparar el desarrollo de software a otras ingenierías asumía que se podía formalizar mucho. Igual que con el tiempo se habían creado mecanismos estandarizados para construir distintos tipos de puentes, o edificios, o barcos, se podían crear mecanismos estandarizados, fiables, replicables y predecibles para desarrollar software.

La perspectiva más agile/craftsman/moderna (y sé que estoy mezclando cosas, pero creo que se entiende a dónde quiero llegar), parte de la idea de que no se puede formalizar tanto, y hace falta tener más flexibilidad. O al menos, que conceptos básicos de otras ingenierías (como estimación de tiempos y costes) no son trasladables al desarrollo de software por la propia naturaleza de la actividad.

Normalmente se suele argumentar que el desarrollo de software tiene muchos más grados de libertad que otras disciplinas en las que las restricciones impuestas por el mundo físico (los materiales para construir un puente son los que son, es fácil determinar la longitud o anchura necesaria, etc.) hacen que sea mucho más sencillo cerrar unos requisitos claros al principio de un proyecto y elegir la forma de llevarlo a cabo.

No tengo ni idea de si eso es así o no en la vida real, pero supongo que habrá casos en los que un arquitecto no tenga que pensar mucho para construir una casa y pueda replicar el mismo proyecto que hizo para la casa anterior, y habrá otros en los que le pidan el nuevo edificio de oficinas de MegaCorp y la parte de definición de requisitos y alcance del proyecto sea bastante más complicada.

En cualquier caso, al final se trata de ver si es una cuestión ontológica o epistemológica. Si realmente la naturaleza del desarrollo de software es tal que no es posible abordarlo mediante técnicas de ingeniería “clásica” y está más cerca del “arte” o de la “artesanía”, o si, en el fondo, estamos limitados por nuestro conocimiento actual.

No hay que olvidar que cuando comparamos el desarrollo de software con disciplinas como la arquitectura estamos comparando algo 100 años de historia con unos cuantos milenios. En los casi 5.000 años que han pasado desde las pirámides de Egipto hasta ahora, la arquitectura ha tenido tiempo de evolucionar. ¿Os imagináis cómo será el desarrollo, no dentro de 5.000 años, sino dentro de 500? ¿Realmente podemos estar tan seguros de que todas esas incertidumbres que vemos ahora no serán fácilmente decidibles entonces?

La verdad es que no tengo ni idea de lo que pasará, pero lo que “la naturaleza del desarrollo lo hace imposible de gestionar como otras ingenierías”, me parece que sólo podría tener sentido dentro del contexto de conocimiento actual y no es tanto “la naturaleza del desarrollo”, sino “con lo que nosotros sabemos hoy del desarrollo”.

No hay posts relacionados.

Viewing all 2713 articles
Browse latest View live