Skip to content

Blog

Advanced Cron

Per gestire l'emergenza Covid-19 è necessario elaborare e trasmettere diversi flussi informativi a diverse piattaforme (piattaforme aziendali, bucket regionali, in formato csv o excel, applicativi di gestione casi, dashboard di aggregazione o analisi dei dati, etc.).

Ho quindi predisposto diversi task che: - richiamano dei piccoli applicativi Perl per estrarre via SQL le informazioni necessarie, le trasformano, e le salvano in file in formato CSV - comprimono i dati, eventualmente con password complesse - trasmettono i dati verso bucket aws, su altri server Qlik o Pentaho - generano mail puntuali o di riepilogo

Il testo che segue non è un "manuale" su come fare le cose, ma un elenco di appunti che mi aiutano a ricordare quali operazioni ho fatto e qual è la logica sottostante.

La schedulazione avviene con il servizio crond. Tuttavia nel corso del tempo il numero di task è diventato numeroso, la configurazione della crontab è diventa complessa, e modificare o monitorare il corretto funzionamento dei task è diventato poco agevole.

Ho quindi voluto riorganizzare il servizio con il seguente sistema. La tabella con i task è minimale, questo un task che necessita di essere eseguito ogni ora:

0  *  *  *  * /home/fede/scripts/hourly_tasks.sh

Questo task a sua volta si occupa di lanciare tutti gli script che devono essere eseguiti ogni ora, gestendone correttamente l'output stdout e stderr nei file di log opportuni:

#!/bin/bash

LOG_FILE=/var/log/covid_out.log
LOG_ERROR=/var/log/covid_err.log

exec 1>>"$LOG_FILE" 2>>"$LOG_ERROR"

./task01.sh &
./task02.sh &
./task03.sh &
...

In questo caso i vari task vengono eseguiti in parallelo, ed ereditano gli output verso i file di log.

Tuttavia per ogni task voglio fare in modo che il log contenga la data e l'ora, ed il nome del task eseguito:

#!/bin/bash
PROGNAME=$(basename $0)

exec  > >(sed --unbuffered "s/^/$(date +'%Y-%m-%d %H:%M:%S'),$PROGNAME: /")
exec 2> >(sed --unbuffered "s/^/$(date +'%Y-%m-%d %H:%M:%S'),$PROGNAME: /" >&2)

...

Il parametro --unbuffered assicura che la singola riga viene processata immediatamente, senza utilizzare un buffer, in modo che data e l'ora presentate nel file di output siano quelle effettive di quando l'output è stato realizzato e che i vari output siano sequenziali.

Inoltre per gestire correttamente eventuali errori, ho predisposto una funzione come segue:

error_exit()
{
        echo "${1:-"Unknown Error"}" 1>&2
        exit 1
}

La struttura completa del task risulta essere questa:

#!/bin/bash
PROGNAME=$(basename $0)

error_exit()
{
        echo "${1:-"Unknown Error"}" 1>&2
        exit 1
}

exec  > >(sed --unbuffered "s/^/$(date +'%Y-%m-%d %H:%M:%S'),$PROGNAME: /")
exec 2> >(sed --unbuffered "s/^/$(date +'%Y-%m-%d %H:%M:%S'),$PROGNAME: /" >&2)

echo "Inizio elaborazione"

perl "extract.pl" -prepare || error_exit "$LINENO: Errore preparazione dati."
perl "extract.pl" -extract > $output || error_exit "$LINENO: Errore estrazione dati."

...

Come scaricare un file più recente da aws via cli:

latest=`$AWS s3 ls s3://bucket/path/documento | sort | tail -1 | awk '{ print $4 }'`                   
$AWS s3 cp --no-progress s3://bucket/path/$latest /home/fede/csv/documento.csv

Per scaricare il file più recente da un altro unix, in questo caso utilizzando sshpass (da valutare ovviamente se è la soluzione opportuna):

latest=$(sshpass -p $pass ssh fede@server 'ls -t /var/export/documento* | head -1')
sshpass -p $pass scp fede@server:/$latest $output_dir/$output_file

Millennium Bug? Quale Millennium Bug?

Commento del 17 gennaio 2020 su un blog, in risposta a una discussione sul Millennium Bug e sugli scenari apocalittici del 1999.

Io c'ero, e sono convinto che gli scenari apocalittici che si sarebbero dovuti verificare esattamente allo scoccare della mezzanotte fossero in gran parte superstizione.

Poi è assolutamente vero che migliaia di programmi (gestionali, contabilità, etc.) calcolavano le date con 2 cifre invece di 4 e che quindi dovevano essere corretti. All'epoca abbiamo lavorato mesi su questo fronte. Alcuni programmi però avrebbero dato risultati errati già mesi prima della fatidica presunta fine del mondo, altri avrebbero dato risultati errati mesi dopo. Soprattutto, non era necessario aspettare la mezzanotte per saperlo, si poteva testare tutto agevolmente in anticipo.

(a dire la verità ci sono programmi che ancora oggi fanno confusione ogni volta che cambia l'anno, e per capire come mai basta guardare gli snippet di codice che girano per il web e che i programmatori inesperti copiano ed incollano sui loro progetti: la maggior parte di questi snippet sono buggati! Anche il calcolo degli anni bisestili non è sempre corretto, ed anche il cambio ora legale/ora solare ha spesso effetti deleteri)

Quella notte i colleghi del servizio informativo avevano una reperibilità speciale, con tanto di radio a pile per essere raggiungibili anche in caso di blackout. Ricordo che il pomeriggio di quel fatidico 31 dicembre i colleghi più anziani, che poi sarebbero rimasti reperibili tutta la notte, erano sollevati dal fatto che le notizie provenienti dall'Asia fossero positive: nessun grave problema. Io, che sono malvagio, li ho spaventati facendo notare come tutti i sistemi unix compresi i nostri dovessero avere il clock sincronizzato con UTC, per cui in Asia non avevano ancora raggiunto la "vera" mezzanotte, anzi nella mia malvagità avevo sganciato una bomba, facendo presente che semmai ci fosse stato qualche disastro questo si sarebbe verificato alla mezzanotte UTC in contemporanea su tutti i sistemi del pianeta. Poi sono andato a festeggiare in montagna :)

Precisazione tecnica: i sistemi Unix sincronizzano tipicamente il clock con UTC (Coordinated Universal Time), che per semplicità potremmo considerare equivalente al fuso orario di Greenwich (GMT), anche se dal punto di vista scientifico non è la stessa cosa. Il mio ragionamento scherzoso non era quindi del tutto sballato: il momento critico era lo stesso per tutti i sistemi sincronizzati a UTC, indipendentemente dal fuso locale. Ma nessuno ci aveva pensato.

Per chiarire meglio il mio pensiero, è vero che di programmi buggati ce n'erano parecchi ed è stato un bene averli sistemati. Clock hardware buggati invece ne sono stati trovati? Gli articoli giornalistici dell'epoca erano più dettati dalla superstizione che non dalla reale conoscenza dei sistemi.

Infine, il vero Millennium Bug avverrà nel 2038, alle 03:14:07 UTC del 19 gennaio, e non a mezzanotte come nel 1999. Ed è ancora troppo sottovalutato.

Write once, read never

Commenti del 6 dicembre 2019 su un blog, in una discussione sui linguaggi di programmazione. Li ho riorganizzati in un unico testo.

Vero, non è il linguaggio quanto chi ci sviluppa.

Ho visto capolavori di stile scritti in Perl (cui è stato assegnato l'infame motto "write once, read never") ed ho visto geroglifici scritti in linguaggi che dovrebbero perlomeno facilitare la scrittura, es. Python.

Però non sono del tutto d'accordo che un linguaggio complesso sia vantaggioso. Da informatico mi piace risolvere problemi complessi in modi semplici. Risolvo problemi complicati perché odio le complicazioni.

Quindi se il linguaggio è complicato, rischi di attrarre gente che le complicazioni le amano! Lo dico perché nel mio ambito di lavoro vedo spesso dei framework complicati, ma lo sono perché sono stati progettati male e senza buon gusto estetico, non perché devono per forza essere così.

Questo il mio punto di vista ;)


C'è poi un problema più generale, che non dipende dal linguaggio: il copia-incolla.

Seguo (e quando ho tempo, partecipo attivamente) la community Stack Overflow, dove gli sviluppatori fanno domande ed altri sviluppatori rispondono. Ci sono dei veri e propri guru che partecipano, ma vi partecipano utenti di tutti i livelli (anche scarsi).

Copiare ed incollare una soluzione che poi finisce in produzione avviene regolarmente, il fatto è che non tutte le soluzioni sono "giuste": magari risolvono immediatamente un particolare problema ma ne introducono altri, sono obsolete, od hanno problemi gravi, anche di sicurezza. E non tutti vogliono approfondire.

Se ne parla ad esempio in questo articolo del blog di Stack Overflow (del novembre 2019):

https://stackoverflow.blog/2019/11/26/copying-code-from-stack-overflow-you-might-be-spreading-security-vulnerabilities/

ci si riferisce principalmente al C/C++ ma è utile per qualsiasi sviluppatore.

Daily coding problem #15: Random element from the stream

Problema:

Given a stream of elements too large to store in memory, pick a random element from the stream with uniform probability.

Scegliere un elemento casuale con probabilità uniforme da un array di dimensione n è semplice:

from random import randrange

foo = ['A', 'B', 'C', 'D']
n = len(foo)

print(foo[randrange(n)])

in realtà vi sarebbe anche il metodo random.choice() o lo stesso metodo della libreria secrets che garantisce sicurezza a livello crittografico.

Se lo stream però è troppo grande per stare in memoria, e non ne conosciamo la dimensione in anticipo, non possiamo applicare il metodo precedente ma dobbiamo utilizzare una diversa strategia. Necessariamente dovremo selezionare un elemento mentre stiamo leggendo lo stream, avendo come unica informazione il numero di elementi ricevuti fino a quel momento, ma non sapendo quanti ne mancano alla fine.

L'elemento corrente avrà probabilità $\(\frac{1}{n}\)$ di essere estratto, dove $\({n}\)$ è il numero di elementi letti dallo stream fino a quel momento. Potremmo selezionarlo in questo modo:

if random.randint(1,n) == n:
    chosen = current_element_from_stream

che cosa succede ad eventuali elementi letti in precedenza? Dobbiamo dimostrare come tutti gli elementi abbiano a loro volta la stessa probabilità $\(\frac{1}{n}\)$ di essere stati scelti.

L'elemento precedente lo abbiamo scelto con probabilità $\(\frac{1}{(n - 1)}\)$ il quale ha probabilità $\(\frac{(n - 1)}{n}\)$ di rimanere, da cui si ricava la sua probabilità che è $\(\frac{1}{(n-1)} \cdot \frac{(n-1)}{n} = \frac{1}{n}\)$.

Il codice completo può essere il seguente (dove lo stream è "simulato" da un array):

import random

def get_random_element(stream):
    chosen = None
    n = 0

    for i in range(len(stream)):
        n=n+1
        if random.randint(1,n) == n:
            chosen = stream[i]

    return chosen

stream = ['A', 'B', 'C', 'D']
print(get_random_element(stream))

Come funziona un buffer overflow

Commenti del 25 ottobre 2019 su un blog, in una discussione sulla sicurezza dei file audio e sui malware. Li ho riorganizzati in un unico testo.

Il buffer overflow è stato uno dei vettori di attacco più comuni, e non è che sia del tutto sparito. I vari sistemi operativi recenti offrono protezioni aggiuntive che, in combinazione con funzionalità hardware, rendono sempre più difficile la cosa. Ma vale la pena capire come funziona.

Un errore classico

Storicamente funzionava così:

  • un programmatore dichiarava un buffer di una certa dimensione, es. 80 caratteri
  • un attaccante malevolo passava in input al programma un parametro troppo lungo, es. 800 caratteri
  • il programma, causa bug, non applicava tutti i controlli necessari e copiava l'input da 800 caratteri sul buffer da 80
  • i caratteri eccedenti andavano a sovrascrivere aree di memoria non previste, che con un po' di fortuna potevano essere associate ad esempio ad altre variabili - ecco che con questo trucco potremmo andare a cambiare dei valori cui non dovremmo avere accesso, e quindi cambiare le logiche di esecuzione del programma
  • meglio ancora, le aree di memoria non previste potevano essere associate allo stack. Quando richiamo una funzione (call) viene inserita nello stack la posizione dell'istruzione che segue la call, in modo da ritornare lì (ret) una volta terminata la funzione. Ecco che modificando i valori inseriti nello stack, tra cui l'elenco delle istruzioni cui riprendere l'esecuzione terminata una funzione, possiamo dire ad un programma di eseguire le istruzioni di una qualsiasi area di memoria, magari proprio all'interno di questi 800 caratteri.

Ecco, quanto descritto è un metodo "classico" causato da un errore "classico": buffer troppo corto, mancanza di adeguati controlli.

Perché funziona

Devi ragionare in termini di linguaggio macchina, e devi pensare a come il tuo codice C o C++ è compilato e a come viene poi eseguito. L'attacco che ho descritto è - era? - molto comune, ed è stato usato con successo diverse volte: la maggior parte dei worm che si diffondevano in rete sfruttavano proprio questa tecnica.

L'attacco consiste nell'inviare una stringa molto più lunga di quanto il programma ha previsto, che contiene:

  1. il codice macchina malevolo da eseguire
  2. l'indirizzo del codice macchina al punto 1

In particolare, l'indirizzo al punto 2 oltre a dover essere calcolato precisamente, è indispensabile che sia perfettamente allineato, in modo che si sovrapponga alla struttura dati che contiene le "informazioni di controllo dell'esecuzione" del programma.

Il buffer può trovarsi dove si vuole, ma la sua posizione relativa potrebbe essere sempre la stessa, ecco perché il sistema funziona. Vari sistemi, come l'Address Space Layout Randomization, cercano appunto di rendere le posizioni relative non prevedibili, per cui questo tipo di attacco è molto più difficile (ma non impossibile).

Un crash non è la fine

In sostanza, è vero che la maggior parte delle volte un bug di questo tipo produce un errore di segmentation fault, ma inviando dei dati malvagi, preparati con grande accuratezza, è possibile far eseguire al programma qualche cosa di non previsto.

Non sempre questi bug sono sfruttabili (exploitabili): si potrebbe riuscire a mandare in crash il programma ma potrebbe non essere possibile controllarne l'esecuzione. Comunque un bel crash promette sempre bene.

Un errore di segmentation fault è un punto di partenza, da solo serve poco se non per mettere in piedi attacchi DoS (denial of service), ma significa che il programma ha qualche bug che potrebbe essere sfruttato per un attacco.

Le protezioni moderne

Questo metodo è sempre più difficile da applicare:

  • i compilatori potrebbero segnalare dei warning
  • gli strumenti di analisi del codice dovrebbero essere in grado di segnalare probabili bug di questo tipo
  • i sistemi operativi nonché i processori offrono protezioni aggiuntive, come l'ASLR, il DEP, lo stack canary

Ci sono comunque anche diverse strategie di attacco.

Nota sui file audio e sulle immagini

Un'ultima precisazione: la frase "non è possibile infettarsi semplicemente scaricando e ascoltando un file audio WAV" andrebbe esplicitata meglio.

Nemmeno con i file JPG, HTML, o Flash sarebbe teoricamente stato possibile infettarsi, nemmeno con le e-mail (sono solo dei banali testi!) o con le chiavette USB, eppure causa programmi vulnerabili è successo più di qualche volta.

Daily coding problem #28: Write an algorithm to justify text

Problem:

Write an algorithm to justify text. Given a sequence of words and an integer line length k, return a list of strings which represents each line, fully justified.

More specifically, you should have as many words as possible in each line. There should be at least one space between each word. Pad extra spaces when necessary so that each line has exactly length k. Spaces should be distributed as equally as possible, with the extra spaces, if any, distributed starting from the left.

If you can only fit one word on a line, then you should pad the right-hand side with spaces.

Each word is guaranteed not to be longer than k.

For example, given the list of words ["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"] and k = 16, you should return the following:

["the quick brown", # 1 extra space on the left

"fox jumps over", # 2 extra spaces distributed evenly

"the lazy dog"] # 4 extra spaces distributed evenly

This looks like a problem that can be quickly solved in a few lines of codes, but it would actually require a little more lines than expected. While the case of a word longer than the given integer k is not specifically covered in the problem description, I assume than such word has to be split into k sized chunks.

The logic is going to be like this:

  • I keep a buffer that holds all words that can fit into a single line. This buffer is empty at first.
  • From the given array I read each word, one by one. I eventually split each word into k-sized chunks.
  • Is the current word / word chunk going to fit into the current line? If there's still space for it I am going to add it to the current line buffer.
  • If the current word / chunk won't fit, I am going to output the current line (all words in the buffer, with proper space justification). The buffer will then be set to contain only the current word.
  • When I am finished reading all words, I am going to output the current line (unless the buffer is empty, which will happen only if the array of words is empty as well)

This is how I am going to call the function:

if __name__ == '__main__':
    print("\n".join(justify(["the", "quick", "brown", "fox",
        "jumps", "over", "the", "lazy", "dog"], 16)))

And this is how my justify function looks like:

def justify(words, k):
    res = []
    current_length = 0
    current_words = []

    for word in words:
        # read each word
        for i in range(0, len(word), k):
            # split each row into k-sized chuncks
            chunk = word[i:i+k]

            # try to add a new word to current_words / current_length
            if len(chunk) + current_length + len(current_words) <= k:
                # can fit
                current_words.append(chunk)
                current_length += len(chunk)
            else:
                # won't fit
                res.append(justify_line(current_words, current_length, k))
                current_words = [chunk]
                current_length = len(chunk)

    if current_words:
        res.append(justify_line(current_words, current_length, k))

    return res

The final part is handled by the justify_line function, that accept as input the array w of words that fits in the current line, the length l of the current words (not considering spaces), and the width of the like k.

(l is not strictly necessary, I have all elements to calculate it inside the function, but since it's alreay available I'll just pass it to the function)

def justify_line(w, l, k):
    if len(w)==1:
        return w[0].ljust(k)
    else:
        narrow_spaces, wider_words = divmod(k - l, len(w)-1)
        ns = " " * narrow_spaces
        ws = " " * (narrow_spaces + 1)

        return ns.join([ws.join(w[0:wider_words+1]), *w[wider_words+1:]])
  • If the current line contains only one word then I'll just return this word left-justified with spaces at the right (as from the problem specs).

  • If instead the line is composed by more words, we have to calculate:

  • the total number of spaces needed (k - l)
  • the minimum number of spaces between each word (total numer of spaces DIV number of words minus one)
  • the number of words that require an additional space (total number of spaces MOD number of words minus one)

For example if I call justify_line(['the', 'quick', 'brown'], 13, 16): - narrow_spaces will be set to 1 (one space between each word) - wider_words will be set to 1 (the first word needs one more extra space after it) - ws.join(w[0:wide_words+1]) will join the first and second words with wider spaces - ns.join() will then join the wider-spaced-words with all remaining words

To make things easier during developement, and to make sure that a little fix won't mess up everything, I wrote this testing unit:

import unittest
import re
from justify_text import justify

class TestStringMethods(unittest.TestCase):

    def test_given(self):
        self.assertEqual(
            justify(
                ["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"], 16),
                ["the  quick brown",
                 "fox  jumps  over",
                 "the   lazy   dog"])

    def test_lengths(self):
        for i in range(1,80):
            j = justify(["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"], i)
            lengths = list(map(lambda x: len(x), j))
            for n in range(0, len(lengths)):
                self.assertEqual(lengths[n], i)

    def test_decrease(self):
        for i in range(1,80):
            j = justify(["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"], i)

            spaces_split = list(map(lambda x: re.split('[^ ]+', x), j))
            for n in range(0, len(spaces_split)):
                self.assertEqual(spaces_split[n][0], '')
                for t in range(1, len(spaces_split[n])-2):
                    self.assertTrue(len(spaces_split[n][t])>=len(spaces_split[n][t+1]))

if __name__ == '__main__':
    unittest.main()
  • test_given: the result of the function has to match the result from the problem specification
  • test_lengths: all strings have to be of length k (k from 1 to 80)
  • test_decrease: all strings have to start with a word, not a space, and the number of spaces from left to right has not to increase

L'IP era vero, il mittente no

Commenti del 26 ottobre 2018 su un blog, in una discussione sul tracciamento degli utenti. Li ho riorganizzati in un unico testo, tagliando le ripetizioni.

Il discorso è complesso, e forse si fa confusione perché si mescolano due problemi diversi. Io li vedo così:

  • molte aziende su internet trattano nostri dati personali che noi non sappiamo di aver dato loro (e con questi dati ci campano, ecco perché la professione di data scientist è sempre più richiesta e sempre più pagata...)
  • molte aziende non sono nemmeno in grado di trattare e proteggere i dati che invece abbiamo affidato loro

Vale anche per i dati sanitari, ad esempio... purtroppo privacy, GDPR etc. vengono visti come adempimenti burocratici da applicare alla lettera senza capire che cosa si sta facendo e perché, col risultato che si possono anche avere sistemi che sulla carta sono compliant ma in pratica fanno acqua da tutte le parti...


Un esempio che mi è capitato in prima persona molti anni fa, e che forse può far capire meglio il senso del mio discorso. Era il 1999, e via mail arrivava un virus. A me e a molti partecipanti di un newsgroup. Il mittente era anonimo, ma l'IP era vero.

A quel tempo non si discuteva su Disqus o sui forum, ma sui newsgroup. Un newsgroup era un elenco di post, cioè nuove discussioni o risposte ad altre discussioni. Ad ogni post era associato il suo autore - anche fittizio, non serviva registrazione - e di ogni post era possibile ottenere l'indirizzo IP da cui era stato inviato.

Volevo capire chi era il vero mittente a partire dall'IP, ma come fare senza scomodare il provider?

Ho pensato di scaricare qualche migliaio di discussioni dai newsgroup, e le ho caricate su di un database con IP, data ed ora. Ho confrontato gli IP delle mail anonime con gli IP nel database, e la cosa ha funzionato perfettamente!

E ho trovato diverse cose interessanti: utenti diversi che condividevano lo stesso IP, un utente che si spacciava per utenti diversi, e l'autore inconsapevole della mail - il vero autore non sapeva di aver mandato il virus, perché il suo PC era infetto.

Corollario: discutere sui newsgroup rendeva pubblicamente associabile il tuo IP al tuo nickname!

Oggi i newsgroup non vanno più di moda, l'IP potrebbe non voler dire molto e gli IP delle discussioni in ogni caso non sono più pubblici (ma il gestore del forum li ha eccome) e solo una piccola percentuale di utenti partecipa alle discussioni - ci sono però altre soluzioni più moderne.

Non sto dicendo che tutti commettano illeciti, sto solo dicendo di fare attenzione che i fornitori di servizi dispongono di molti più dati su di noi rispetto a quelli che noi pensiamo di dare, e i dati sono come i maiali, non si butta via niente!

Questo lavoro è probabilmente la cosa più importante che ho fatto in quegli anni, perché dimostra di aver capito come funziona davvero il mondo: le tracce digitali raccontano una verità che spesso non coincide con l'intenzione di chi le ha lasciate.


Il punto però non è solo Facebook o Google presi singolarmente. Il punto è come funziona il web nel suo insieme.

Oggi i newsgroup sono stati sostituiti, ma il meccanismo di correlazione non è scomparso: si è semplicemente spostato sui circuiti pubblicitari e sui widget di terze parti.

Se un utente visita in sequenza siti diversi che condividono lo stesso sistema di commenti, lo stesso fornitore di statistiche o la medesima rete di banner pubblicitari, il fornitore terzo dispone di tutti gli elementi per ricostruire la cronologia di navigazione, anche in assenza di un account registrato o di un'autenticazione esplicita.

Le tecnologie cambiano, ma la dinamica resta la stessa: le informazioni che condividiamo sono molte più di quelle che pensiamo di trasmettere.

Show text files width

I am often working with fixed width text files. Sometimes text files aren't produced properly, so I have to make sure that all rows share the same width.

This is one liner perl thay I use quite often, on a Windows shell:

c:\> perl -lne "$h{length($_)}=1; END{print join \"\n\", sort keys %h}" source.txt

this is how it looks like in Linux, with a simpler way of quoting:

$ perl -lne '$h{length($_)}=1; END{print join "\n", sort keys %h}' source.txt
  • the -l flag handles newlines
  • the -n flag adds a while loop

for each line we calculate its length with length($_), we set the hash element $h{length($_)} to 1, which means that we have at least one row with the calculated length. When we are finished scanning the file, we print the list of hash keys in sorted order, which is the list of calculated lengths.

If the one liner returns only one length then we are fine, all rows share the same, hopefully correct, width. If it returns more lengths then I'll have to further investigate the problem.

Cut a fixed width text file in CMD

I recently had the need to cut a fixed width text file at a certain column, and I had to do it in the Windows 10 world, without any particular utility.

In linux it would be rather simple, thanks to the cut command:

$ cut -c -10 example.txt

On my windows machines there's always Perl available, and one alternative solution would be using one liner substitution:

perl -pe s/(.{10}).*/$1/ example.txt

but on my colleagues machines there are no particular utilities installed, so I had to create the following script:

@echo off
setlocal EnableDelayedExpansion
for /f "delims=" %%r in (example.txt) do (
    set s=%%r
    echo !s:~0,10!
)

it's still possible to use a single line command, without the need of a .cmd script, but firts we have to launch the prompt with a special parameter cmd /v which indicates that delayed expansion is enabled (it is disabled by default to be compatible with MS-DOS 2.0 batch files!). After we launch cmd /v the command becomes:

for /f "delims=" %r in (example.txt) do @(set s=%r && echo !s:~0,10!)

not really intuitive, is it? I think I am missing my teenager days when I used to program Assembly x86: that was complex, but for good reasons. Cmd scripting is still complicated, but for no knowns reasons! Also, please note that this solution is painfully slow.

Pratical use of Sql::Textify

I know that perl is considered out of fashion nowadays, but for some tasks it's still a good and handy choice. Here's a practical use of my module Sql::Textify. Every morning I need to check the status of some tasks, like the number of times my web services have been called the day before, with some performance analysis and the number of errors. At the same time I want to know if my pentaho tasks are all finished. And I want to know if all my backups are updated.

Luckily I managed to write all the info I need into some sql tables, updated automatically. So basically I just need to login to my Adminer.php instance and run some queries or some views. But how if I need to share those info to my colleagues? I can quickly export a dataseto to a Markdown text file, ready to be beautified with Markdown Here plugin.

But I wanted to make things more practical and faster. My initial idea was to make a Mason2 plugin that calls Sql::Textify and this might still be a good idea to handle complex contents, but since my content is often simple and Mason2 is out of fashion anyways, I wrote a simple script that handles everyting.

Here's an example. First we need a very simple template html page:

<html>
<header>
<style>
..insert a good style..
</style>
<title>[[ $title ]]</title>
</header>
<body>
[[ $body ]]
</body>
</html>

then the perl script is like this:

use strict;
use warnings;
use SQL::Textify;
use File::Slurp;

my $t = Sql::Textify->new(
    conn => "dbi:SQLite:dbname=samples.db",
    username => "username",
    password => "password",
    format => 'html'
);

# read the template file
my $html = read_file( 'main.html' );

# set the title
my $title = "Report Indicizzazione";

# set the content of the main component
my $body = <<'BODY';
<h1>Daily report</h1>

<h2>Public Web Service</h2>

<% $t->textify("select * from view_web_service_status where eventdate>=currentdate"); %>

<h2>Pentaho Integrations Log</h2>

<% "select * from view_pentaho_log" | $t->textify %>
BODY

# first syntax, evaluates code between <% and %>
$body =~ s /\<\%\s+(.*?);\s+\%\>/$1/eeg;

# second syntax, apply $t->textify to the query (works as a filter)
$body =~ s /\<\%\s+(\".*?\")\s*\|(.*?)\s+\%\>/"$2\($1\)"/eeg;

# convert all [[ $variable ]] to the actual value
$html =~ s/\[\[ (\$\w*) \]\]/$1/eeg;

print $html;

This is not a perfect solution, but I just needed a quick tool to export my data and I wanted it to look good. The syntax is inspired somehow to the Mason2 syntax. A real Mason2/(or anything else) component of course is much more flexible but at the same time is little slower to write and more difficult to mantain.