<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>shell &amp;mdash; Cyberdyne Systems</title>
    <link>https://noblogo.org/aytin/tag:shell</link>
    <description>&#34;Fare o non fare. Non c&#39;è provare!&#34;</description>
    <pubDate>Thu, 30 Apr 2026 09:55:31 +0000</pubDate>
    <item>
      <title>Reload Radio Deejay in bash</title>
      <link>https://noblogo.org/aytin/reload-radio-deejay-in-bash</link>
      <description>&lt;![CDATA[(pubblicato il 22 marzo 2021)&#xA;vecchia radio&#xA;small/iPhoto by Nothing Ahead on a href=&#34;https://www.pexels.com/it-it/foto/shallow-focus-foto-di-una-vecchia-radio-3199028/&#34;Pexels.com/a/i/small&#xA;&#xA;Questo script permette di scaricare le puntate intere di Ciao Belli o DeeJay Time da Deejay Reload perché le ascolto offline praticamente da sempre. In alternativa, è possibile ascoltare direttamente il live streaming delle trasmissioni.&#xA;!--more--&#xA;I pattern che uso per lo scraping sono molto elementari e sono ovviamente sensibili ai minimi cambiamenti che avvengono sulle pagine di Radio Deejay.&#xA;&#xA;La versione attuale è attiva da un paio di mesi a questa parte perché il pattern prima era leggermente diverso. E più si va indietro, più le differenze aumentano. Per dirne una, lo streaming hls è molto recente, Prima di esso, il reload era disponibile solo in mp3. Questa sarà la 15supa/sup versione dello script 🙂&#xA;&#xA;Il download restituisce un file in mp3 o aac. Quest&#39;ultimo ottenuto in maniera rudimentale estrando i singoli segmenti aac ottenuti dall&#39;analisi della playlist hls e ricombinati attraverso ffmpeg.&#xA;&#xA;In linea di principio sarebbe possibile estendere lo script aumentando la scelta delle trasmissioni disponibili e, volendo migliorare l&#39;interazione con l&#39;utente sacrificando l&#39;automatismo e la &#34;scriptabilità&#34;, dotandolo di un menù.&#xA;&#xA;help() {&#xA;    echo &#34;$1&#34;&#xA;    echo&#xA;    echo &#34;Usa come: wgetdeejay.sh YYYYMMDD prg format&#34;&#xA;    echo &#34;Es. wgetdeejay.sh 2019062 cb mp3  [Scarica Ciao Belli in mp3]&#34;&#xA;    echo &#34;    wgetdeejay.sh 2019062 djt aac [Scarica Deejay Time in aac]&#34;&#xA;    echo &#34;    wgetdeejay.sh live            [Deejay Live]&#34;&#xA;}&#xA;&#xA;download() {&#xA;    if [[ ${FMT} == &#34;mp3&#34; || -z ${FMT} ]]; then&#xA;        if ! checkanddownloadmp3; then&#xA;            return 1&#xA;        fi&#xA;    elif [[ ${FMT} == &#34;aac&#34; ]]; then&#xA;        if ! checkaac; then&#xA;            return 1&#xA;        else&#xA;            downloadaac&#xA;        fi&#xA;    else&#xA;        help &#34;Formato non corretto.&#34;&#xA;        return 1&#xA;    fi&#xA;}&#xA;&#xA;checkanddownloadmp3() {&#xA;    #if ! wget -q https://${HOST}/${DATAURL}/episodes/${TITLE}/${DATA}.mp3 -O &#34;${PRG}${DATA}.mp3&#34;; then&#xA;    echo https://${HOST}/${DATAURL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3&#xA;    if ! wget https://${HOST}/${DATAURL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3 -O &#34;${PRG}${DATA}.mp3&#34;; then&#xA;        help &#34;Puntata di ${TITLE} del ${ERRDATA}, non trovata.&#34;&#xA;        return 1&#xA;    fi&#xA;}&#xA;&#xA;checkaac() {&#xA;    #if ! wget -qO- https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-010000/hls-010000.m3u8 1  /dev/null; then&#xA;    echo https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8&#xA;    if ! wget -qO- https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 1  /dev/null; then&#xA;        help &#34;Puntata di ${TITLE} del ${ERRDATA}, non trovata.&#34;&#xA;        return 1&#xA;    else&#xA;        return 0&#xA;    fi&#xA;}&#xA;&#xA;downloadaac() {&#xA;    echo -n &#34;Scarico la traccia............. &#34;&#xA;    ffmpeg -i https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 -c copy $HOME/deejay/&#34;${PRG}${DATA}&#34;.aac 2  /dev/null&#xA;    echo &#34;Fatto.&#34;&#xA;&#xA;}&#xA;&#xA;DATA=$1&#xA;PRG=$2&#xA;FMT=$3&#xA;&#xA;if [[ $1 == &#34;live&#34; ]]; then&#xA;    mpv http://radiodeejay-lh.akamaihd.net/i/RadioDeejayLive1@189857/master.m3u8&#xA;else&#xA;    case &#34;${PRG}&#34; in&#xA;        &#34;cb&#34;)&#xA;            HOST=&#34;media.deejay.it&#34;&#xA;            TITLE=&#34;ciaobelli&#34;&#xA;            ;;&#xA;        &#34;djt&#34;)&#xA;            HOST=&#34;media.deejay.it&#34;&#xA;            TITLE=&#34;deejaytime&#34;&#xA;&#xA;            ;;&#xA;        &#34;live&#34;)&#xA;            TITLE=&#34;live&#34;&#xA;            ;;&#xA;        *)&#xA;            help &#34;Programma Deejay mancante o non esistente.&#34;&#xA;            exit 1&#xA;    esac&#xA;&#xA;    DATAURL=&#34;${DATA:0:4}/${DATA:4:2}/${DATA:6:2}&#34;&#xA;    ERR_DATA=&#34;${DATA:6:2}-${DATA:4:2}-${DATA:0:4}&#34;&#xA;&#xA;    if [[ -z ${DATA:0:4} ]]; then help &#34;Manca l&#39;anno.&#34;; exit 1&#xA;    elif [[ -z ${DATA:4:2} ]]; then help &#34;Manca il mese.&#34;; exit 1&#xA;    elif [[ -z ${DATA:6:2} ]]; then help &#34;Manca il giorno.&#34;; exit 1&#xA;    elif ! download; then exit 1&#xA;    fi&#xA;fi&#xA;&#xA;#bash #shell #scripting]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 22 marzo 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/gx5yE0mToMHs/pgHaEJhxeXnpa868gj4wz1feQ3PMi6077g1Q1eFd.jpg" alt="vecchia radio">
<small></i>Photo by Nothing Ahead on <a href="https://www.pexels.com/it-it/foto/shallow-focus-foto-di-una-vecchia-radio-3199028/" rel="nofollow">Pexels.com</a></i></small></p>

<p>Questo script permette di scaricare le puntate intere di <strong>Ciao Belli</strong> o <strong>DeeJay Time</strong> da Deejay Reload perché le ascolto offline praticamente da sempre. In alternativa, è possibile ascoltare direttamente il live streaming delle trasmissioni.

I pattern che uso per lo scraping sono molto elementari e sono ovviamente sensibili ai minimi cambiamenti che avvengono sulle pagine di <a href="https://www.deejay.it" rel="nofollow">Radio Deejay</a>.</p>

<p>La versione attuale è attiva da un paio di mesi a questa parte perché il pattern prima era leggermente diverso. E più si va indietro, più le differenze aumentano. Per dirne una, lo <em>streaming hls</em> è molto recente, Prima di esso, il reload era disponibile solo in mp3. Questa sarà la 15<sup>a</sup> versione dello script 🙂</p>

<p>Il download restituisce un file in mp3 o aac. Quest&#39;ultimo ottenuto in maniera rudimentale estrando i singoli segmenti aac ottenuti dall&#39;analisi della playlist hls e ricombinati attraverso ffmpeg.</p>

<p>In linea di principio sarebbe possibile estendere lo script aumentando la scelta delle trasmissioni disponibili e, volendo migliorare l&#39;interazione con l&#39;utente sacrificando l&#39;automatismo e la “scriptabilità”, dotandolo di un menù.</p>

<pre><code class="language-bash">help() {
    echo &#34;$1&#34;
    echo
    echo &#34;Usa come: wget_deejay.sh &lt;YYYYMMDD&gt; &lt;prg&gt; &lt;format&gt;&#34;
    echo &#34;Es. wget_deejay.sh 2019062 cb mp3  [Scarica Ciao Belli in mp3]&#34;
    echo &#34;    wget_deejay.sh 2019062 djt aac [Scarica Deejay Time in aac]&#34;
    echo &#34;    wget_deejay.sh live            [Deejay Live]&#34;
}



download() {
    if [[ ${FMT} == &#34;mp3&#34; || -z ${FMT} ]]; then
        if ! check_and_download_mp3; then
            return 1
        fi
    elif [[ ${FMT} == &#34;aac&#34; ]]; then
        if ! check_aac; then
            return 1
        else
            download_aac
        fi
    else
        help &#34;Formato non corretto.&#34;
        return 1
    fi
}



check_and_download_mp3() {
    #if ! wget -q https://${HOST}/${DATA_URL}/episodes/${TITLE}/${DATA}.mp3 -O &#34;${PRG}_${DATA}.mp3&#34;; then
    echo https://${HOST}/${DATA_URL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3
    if ! wget https://${HOST}/${DATA_URL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3 -O &#34;${PRG}_${DATA}.mp3&#34;; then
        help &#34;Puntata di ${TITLE} del ${ERR_DATA}, non trovata.&#34;
        return 1
    fi
}



check_aac() {
    #if ! wget -qO- https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-010000/hls-010000.m3u8 1&gt;/dev/null; then
    echo https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8
    if ! wget -qO- https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 1&gt;/dev/null; then
        help &#34;Puntata di ${TITLE} del ${ERR_DATA}, non trovata.&#34;
        return 1
    else
        return 0
    fi
}


download_aac() {
    echo -n &#34;Scarico la traccia............. &#34;
    ffmpeg -i https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 -c copy $HOME/deejay/&#34;${PRG}_${DATA}&#34;.aac 2&gt;/dev/null
    echo &#34;Fatto.&#34;

}


DATA=$1
PRG=$2
FMT=$3


if [[ $1 == &#34;live&#34; ]]; then
    mpv http://radiodeejay-lh.akamaihd.net/i/RadioDeejay_Live_1@189857/master.m3u8
else
    case &#34;${PRG}&#34; in
        &#34;cb&#34;)
            HOST=&#34;media.deejay.it&#34;
            TITLE=&#34;ciao_belli&#34;
            ;;
        &#34;djt&#34;)
            HOST=&#34;media.deejay.it&#34;
            TITLE=&#34;deejay_time&#34;

            ;;
        &#34;live&#34;)
            TITLE=&#34;live&#34;
            ;;
        *)
            help &#34;Programma Deejay mancante o non esistente.&#34;
            exit 1
    esac


    DATA_URL=&#34;${DATA:0:4}/${DATA:4:2}/${DATA:6:2}&#34;
    ERR_DATA=&#34;${DATA:6:2}-${DATA:4:2}-${DATA:0:4}&#34;

    if [[ -z ${DATA:0:4} ]]; then help &#34;Manca l&#39;anno.&#34;; exit 1
    elif [[ -z ${DATA:4:2} ]]; then help &#34;Manca il mese.&#34;; exit 1
    elif [[ -z ${DATA:6:2} ]]; then help &#34;Manca il giorno.&#34;; exit 1
    elif ! download; then exit 1
    fi
fi
</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:shell" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">shell</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/reload-radio-deejay-in-bash</guid>
      <pubDate>Tue, 28 Feb 2023 20:16:39 +0000</pubDate>
    </item>
    <item>
      <title>Generazione OTP via bash</title>
      <link>https://noblogo.org/aytin/generazione-otp-via-bash</link>
      <description>&lt;![CDATA[(pubblicato il 30 gennaio 2021)&#xA;otp&#xA;smalliFonte: a href=&#34;https://www.flaticon.com/free-icons/otp&#34; title=&#34;otp icons&#34;Otp icons created by Chanut-is-Industries - Flaticon/a/i/small&#xA;&#xA;Per buona parte del mio tempo sul pc apro una shell, per cui trovo comodo spostare il meno possibile le mani dalla tastiera. Usando spesso 2FA, ho pensato ad un modo per avere un OTP usando bash.&#xA;!--more--&#xA;Il tool che fa al caso mio è oathtool che, per i miei scopi, è sufficiente invocare in questo modo:&#xA;oathtool --totp -b key&#xA;dove \key\ è una stringa base32 bella lunga.&#xA;&#xA;Chiaramente non voglio doverla scrivere ogni volta, nè voglio lasciarla in chiaro. Occorre che:&#xA;&#xA;la chiave sia cifrata e decifrata solamente quando occorre;&#xA;l&#39;output non arrivi su stdout per non doverlo copiare a mia volta (e lasciarlo comunque visibile)&#xA;&#xA;Al punto 1. si risponde con gpg o openssl.&#xA;&#xA;Al punto 2. si risponde con xsel (Gnu/Linux) o pbcopy (MacOS) che trasferiscono l&#39;output di un comando nella clipboard.&#xA;oathtool --totp -b &lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &lt;filechiavecifrato) | xsel -bi&#xA;E veniamo quindi alla:&#xA;Versione 1&#xA;!/bin/bash&#xA;&#xA;# Path del file cifrato contentente la chiave # supponiamo sia di google&#xA;KEY2FAFILE=&#34;$1&#34;&#xA;&#xA;oathtool --totp -b &lt;&lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &#34;${KEY2FAFILE}&#34;) | xsel -bi&#xA;Non è nemmeno uno script. Potrebbe essere direttamente un alias.&#xA;&#xA;ustrongVANTAGGI/strong/u&#xA;&#xA;Estremamente compatto&#xA;Si sfrutta nativamente l&#39;autocomplete col TAB per i file da dare all&#39;input&#xA;L&#39;univocità dei nomi è garantita dal file system&#xA;&#xA;ustrongSVANTAGGI/strong/u&#xA;&#xA;Crea un file per ogni codice e, alla lunga potrebbe generare confusione&#xA;Necessità di creare una password di cifratura per ogni file (potenzialmente tutte diverse)&#xA;&#xA;Versione 2&#xA;Mi occorre quindi un file nome-valore con separatore. Ad es. il simbolo  &#34;:&#34;&#xA;account1:JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA&#xA;account2:JDNBLASPUQRIEKM89478JMFKOVJDOQKJ&#xA;account3:OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF&#xA;account4:MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ&#xA;    &#xA;    ...&#xA;    &#xA;accountn:IWPQSLNCGK49TOWODIR483IRIWOFOPIQO&#xA;Nello script, la variabile KEYS\2FA\FILE memorizza il full path del file cifrato dei codici, mentre l&#39;account deve essere fornito in input. In caso contrario, viene restituita la lista degli account disponibili.&#xA;&#xA; Se l&#39;account è valido, il file viene decifrato e viene estratto il corrispondente codice che viene memorizzato direttamente nella clipboard.&#xA;!/usr/bin/bash&#xA;&#xA;Path del file cifrato contentente i codici 2FA&#xA;KEYS2FAFILE=&#34;path/keys2fa.gpg&#34;&#xA;&#xA;Acquisisce il nome account&#xA;ACCOUNT=$1&#xA;&#xA;Decifra e carica in ram il contenuto del file cifrato&#xA;KEYS2FAFILEDEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${KEYS2FAFILE}&#34; 2  /dev/null)&#xA;&#xA;Se non c&#39;è nessun argomento, restituisce la lista degli account&#xA;if [[ -z &#34;${ACCOUNT}&#34; ]]; then&#xA;    while read LINE; do&#xA;        cut -d&#34;:&#34; -f 1 &lt;&lt;&lt; ${LINE}&#xA;    done &lt;&lt;&lt; $(echo &#34;${KEYS2FAFILEDEC}&#34;)&#xA;    exit&#xA;else&#xA;    # Estrae l&#39;elemento contenente l&#39;id dato in input&#xA;    KEY=$(echo &#34;${KEYS2FAFILEDEC}&#34; | grep ${ACCOUNT} | cut -d&#34;:&#34; -f 2)&#xA;&#xA;    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard&#xA;    # altrimenti esce con errore&#xA;    if [[ -z &#34;${KEY}&#34; ]]; then&#xA;        echo &#34;Account non esistente&#34;&#xA;        exit 1&#xA;    else&#xA;        # Calcola l&#39;otp e lo trasferisce nella clipboard con pbcopy&#xA;        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi&#xA;        echo &#34;Fatto.&#34;&#xA;    fi&#xA;fi&#xA;ustrongVANTAGGI/strong/u&#xA;&#xA;Unico entry-point per la gestione degli account (un&#39;unica password per la cifratura del file&#xA;Più semplice da allocare&#xA;&#xA;ustrongSVANTAGGI/strong/u&#xA;&#xA;L&#39;univocità degli account deve essere garantita dall&#39;utente.&#xA;Perdita della funzionalità di autocomplete in fase di inserimento dati&#xA;&#xA;Provo ad esplorare anche una struttura differente, più espressiva se vogliamo. Invece di un file nome-valore, uso un json così strutturato:&#xA;&#xA;Versione 3&#xA;{&#xA;  &#34;accounts&#34;: [&#xA;    {&#xA;      &#34;id&#34;: &#34;account1&#34;,&#xA;      &#34;secret&#34;: &#34;JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA&#34;&#xA;    },&#xA;    {&#xA;      &#34;id&#34;: &#34;account2&#34;,&#xA;      &#34;secret&#34;: &#34;JDNBLASPUQRIEKM89478JMFKOVJDOQKJ&#34;&#xA;    },&#xA;    {&#xA;      &#34;id&#34;: &#34;account3&#34;,&#xA;      &#34;secret&#34;: &#34;OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF&#34;&#xA;    },&#xA;    {&#xA;      &#34;id&#34;: &#34;account4&#34;,&#xA;      &#34;secret&#34;: &#34;MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ&#34;&#xA;    },&#xA;    &#xA;    ...&#xA;    &#xA;    {&#xA;      &#34;id&#34;: &#34;accountn&#34;,&#xA;      &#34;secret&#34;: &#34;IWPQSLNCGK49TOWODIR483IRIWOFOPIQO&#34;&#xA;    }&#xA;  ]&#xA;}&#xA;Un array, accounts, di oggetti nome-valore.&#xA;&#xA;Lo script che fa il parsing e l&#39;estrazione non è molto diverso dal precedente.&#xA;!/usr/local/bin/bash&#xA;&#xA;Path del file cifrato contentente i codici 2FA&#xA;JSON2FAFILE=path/json2fa.gpg&#xA;&#xA;Acquisisce il nome account&#xA;ACCOUNT=$1&#xA;&#xA;Carica in ram il contenuto del file json cifrato&#xA;JSON2FAFILEDEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${JSON2FAFILE}&#34; 2  /dev/null)&#xA;&#xA;Se non c&#39;è nessun argomento, restituisce la lista degli account&#xA;- jq -c &#39;.accounts[]&#39; restituisce una lista contenenente tante linee per quanti sono gli elementi.&#xA;- La lista viene data in pasto al ciclo while attraverso l&#39;operatore &lt;&lt;&lt; (here-string)&#xA;- Da ogni linea viene estratto il valore del campo &#34;id&#34; sempre attraverso l&#39;operatore &lt;&lt;&lt;&#xA;&#xA;if [[ -z &#34;${ACCOUNT}&#34; ]]; then&#xA;    while read LINE; do&#xA;        jq -r &#39;.id&#39; &lt;&lt;&lt; ${LINE}&#xA;    done &lt;&lt;&lt; $(echo &#34;${JSON2FAFILEDEC}&#34; | jq -c &#39;.accounts[]&#39;)&#xA;    exit&#xA;else&#xA;    # Estrae dall&#39;array l&#39;elemento contenente l&#39;id dato in input&#xA;    KEY=$(jq -r --arg ID ${ACCOUNT} &#39;.accounts[] | select(.id | contains($ID)).secret&#39; &lt;&lt;&lt; ${JSON2FAFILE_DEC})&#xA;&#xA;    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard&#xA;    # altrimenti esce con errore&#xA;    if [[ -z &#34;${KEY}&#34; ]]; then&#xA;        echo &#34;Account non esistente&#34;&#xA;        exit 1&#xA;    else&#xA;        # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel&#xA;        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi&#xA;        echo &#34;Fatto.&#34;&#xA;    fi&#xA;fi&#xA;#bash #otp #scripting #shell ]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 30 gennaio 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/Jm77gCXX9v4y/7IVttwKSmhThLBGuFctXh2BMZau1cgRi2Grgjhmh.jpg" alt="otp">
<small><i>Fonte: <a href="https://www.flaticon.com/free-icons/otp" title="otp icons" rel="nofollow">Otp icons created by Chanut-is-Industries – Flaticon</a></i></small></p>

<p>Per buona parte del mio tempo sul pc <a href="https://noblogo.org/aytin/merge-delle-history-in-tempo-reale" rel="nofollow">apro una shell</a>, per cui trovo comodo spostare il meno possibile le mani dalla tastiera. Usando spesso 2FA, ho pensato ad un modo per avere un OTP usando bash.

Il tool che fa al caso mio è <a href="http://www.nongnu.org/oath-toolkit/oathtool.1.html" rel="nofollow">oathtool</a> che, per i miei scopi, è sufficiente invocare in questo modo:</p>

<pre><code class="language-bash">oathtool --totp -b &lt;key&gt;
</code></pre>

<p>dove <em>&lt;key&gt;</em> è una stringa base32 bella lunga.</p>

<p>Chiaramente non voglio doverla scrivere ogni volta, nè voglio lasciarla in chiaro. Occorre che:</p>
<ol><li>la chiave sia cifrata e decifrata solamente quando occorre;</li>
<li>l&#39;output non arrivi su stdout per non doverlo copiare a mia volta (e lasciarlo comunque visibile)</li></ol>

<p>Al punto 1. si risponde con <a href="https://www.gnupg.org" rel="nofollow">gpg</a> o <a href="https://www.openssl.org" rel="nofollow">openssl</a>.</p>

<p>Al punto 2. si risponde con <strong>xsel</strong> (Gnu/Linux) o <strong>pbcopy</strong> (MacOS) che trasferiscono l&#39;output di un comando nella clipboard.</p>

<pre><code class="language-bash">oathtool --totp -b &lt;&lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &lt;file_chiave_cifrato&gt;) | xsel -bi
</code></pre>

<p>E veniamo quindi alla:</p>

<h2 id="versione-1">Versione 1</h2>

<pre><code class="language-bash">#!/bin/bash

# Path del file cifrato contentente la chiave # supponiamo sia di google
KEY_2FA_FILE=&#34;$1&#34;

oathtool --totp -b &lt;&lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &#34;${KEY_2FA_FILE}&#34;) | xsel -bi
</code></pre>

<p>Non è nemmeno uno script. Potrebbe essere direttamente un alias.</p>

<p><u><strong>VANTAGGI</strong></u></p>
<ul><li>Estremamente compatto</li>
<li>Si sfrutta nativamente l&#39;autocomplete col TAB per i file da dare all&#39;input</li>
<li>L&#39;univocità dei nomi è garantita dal file system</li></ul>

<p><u><strong>SVANTAGGI</strong></u></p>
<ul><li>Crea un file per ogni codice e, alla lunga potrebbe generare confusione</li>
<li>Necessità di creare una password di cifratura per ogni file (potenzialmente tutte diverse)</li></ul>

<h2 id="versione-2">Versione 2</h2>

<p>Mi occorre quindi un file nome-valore con separatore. Ad es. il simbolo  “:”</p>

<pre><code>account_1:JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA
account_2:JDNBLASPUQRIEKM89478JMFKOVJDOQKJ
account_3:OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF
account_4:MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ
    
    ...
    
account_n:IWPQSLNCGK49TOWODIR483IRIWOFOPIQO
</code></pre>

<p>Nello script, la variabile <strong>KEYS_2FA_FILE</strong> memorizza il full path del file cifrato dei codici, mentre l&#39;account deve essere fornito in input. In caso contrario, viene restituita la lista degli account disponibili.</p>

<p> Se l&#39;account è valido, il file viene decifrato e viene estratto il corrispondente codice che viene memorizzato direttamente nella clipboard.</p>

<pre><code class="language-bash">#!/usr/bin/bash

# Path del file cifrato contentente i codici 2FA
KEYS_2FA_FILE=&#34;&lt;path&gt;/keys_2fa.gpg&#34;

# Acquisisce il nome account
ACCOUNT=$1

# Decifra e carica in ram il contenuto del file cifrato
KEYS_2FA_FILE_DEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${KEYS_2FA_FILE}&#34; 2&gt;/dev/null)

# Se non c&#39;è nessun argomento, restituisce la lista degli account
if [[ -z &#34;${ACCOUNT}&#34; ]]; then
    while read LINE; do
        cut -d&#34;:&#34; -f 1 &lt;&lt;&lt; ${LINE}
    done &lt;&lt;&lt; $(echo &#34;${KEYS_2FA_FILE_DEC}&#34;)
    exit
else
    # Estrae l&#39;elemento contenente l&#39;id dato in input
    KEY=$(echo &#34;${KEYS_2FA_FILE_DEC}&#34; | grep ${ACCOUNT} | cut -d&#34;:&#34; -f 2)

    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard
    # altrimenti esce con errore
    if [[ -z &#34;${KEY}&#34; ]]; then
        echo &#34;Account non esistente&#34;
        exit 1
    else
        # Calcola l&#39;otp e lo trasferisce nella clipboard con pbcopy
        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi
        echo &#34;Fatto.&#34;
    fi
fi
</code></pre>

<p><u><strong>VANTAGGI</strong></u></p>
<ul><li>Unico entry-point per la gestione degli account (un&#39;unica password per la cifratura del file</li>
<li>Più semplice da allocare</li></ul>

<p><u><strong>SVANTAGGI</strong></u></p>
<ul><li>L&#39;univocità degli account deve essere garantita dall&#39;utente.</li>
<li>Perdita della funzionalità di autocomplete in fase di inserimento dati</li></ul>

<p>Provo ad esplorare anche una struttura differente, più espressiva se vogliamo. Invece di un file nome-valore, uso un json così strutturato:</p>

<h2 id="versione-3">Versione 3</h2>

<pre><code>{
  &#34;accounts&#34;: [
    {
      &#34;id&#34;: &#34;account_1&#34;,
      &#34;secret&#34;: &#34;JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA&#34;
    },
    {
      &#34;id&#34;: &#34;account_2&#34;,
      &#34;secret&#34;: &#34;JDNBLASPUQRIEKM89478JMFKOVJDOQKJ&#34;
    },
    {
      &#34;id&#34;: &#34;account_3&#34;,
      &#34;secret&#34;: &#34;OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF&#34;
    },
    {
      &#34;id&#34;: &#34;account_4&#34;,
      &#34;secret&#34;: &#34;MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ&#34;
    },
    
    ...
    
    {
      &#34;id&#34;: &#34;account_n&#34;,
      &#34;secret&#34;: &#34;IWPQSLNCGK49TOWODIR483IRIWOFOPIQO&#34;
    }
  ]
}
</code></pre>

<p>Un array, <em>accounts</em>, di oggetti nome-valore.</p>

<p>Lo script che fa il parsing e l&#39;estrazione non è molto diverso dal precedente.</p>

<pre><code class="language-bash">#!/usr/local/bin/bash

# Path del file cifrato contentente i codici 2FA
JSON_2FA_FILE=&lt;path&gt;/json_2fa.gpg

# Acquisisce il nome account
ACCOUNT=$1

# Carica in ram il contenuto del file json cifrato
JSON_2FA_FILE_DEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${JSON_2FA_FILE}&#34; 2&gt;/dev/null)

# Se non c&#39;è nessun argomento, restituisce la lista degli account
# - jq -c &#39;.accounts[]&#39; restituisce una lista contenenente tante linee per quanti sono gli elementi.
# - La lista viene data in pasto al ciclo while attraverso l&#39;operatore &lt;&lt;&lt; (here-string)
# - Da ogni linea viene estratto il valore del campo &#34;id&#34; sempre attraverso l&#39;operatore &lt;&lt;&lt;

if [[ -z &#34;${ACCOUNT}&#34; ]]; then
    while read LINE; do
        jq -r &#39;.id&#39; &lt;&lt;&lt; ${LINE}
    done &lt;&lt;&lt; $(echo &#34;${JSON_2FA_FILE_DEC}&#34; | jq -c &#39;.accounts[]&#39;)
    exit
else
    # Estrae dall&#39;array l&#39;elemento contenente l&#39;id dato in input
    KEY=$(jq -r --arg ID ${ACCOUNT} &#39;.accounts[] | select(.id | contains($ID)).secret&#39; &lt;&lt;&lt; ${JSON_2FA_FILE_DEC})

    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard
    # altrimenti esce con errore
    if [[ -z &#34;${KEY}&#34; ]]; then
        echo &#34;Account non esistente&#34;
        exit 1
    else
        # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel
        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi
        echo &#34;Fatto.&#34;
    fi
fi
</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:otp" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">otp</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a> <a href="/aytin/tag:shell" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">shell</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/generazione-otp-via-bash</guid>
      <pubDate>Tue, 28 Feb 2023 07:59:24 +0000</pubDate>
    </item>
    <item>
      <title>Merge delle history in tempo reale</title>
      <link>https://noblogo.org/aytin/merge-delle-history-in-tempo-reale</link>
      <description>&lt;![CDATA[(pubblicato il 27 gennaio 2021)&#xA;coding&#xA;smallia href=https://www.rawpixel.com/image/432709/free-photo-image-coding-cyber-softwareCoding on a computer screen/a by a href=https://www.rawpixel.com/markusspiskeMarkus Spiske/a is licensed under a href=https://creativecommons.org/publicdomain/zero/1.0/CC-CC0 1.0/a/i/small&#xA;1. Introduzione&#xA;&#xA;Quando si eseguono varie istanze di shell bash (zsh mi pare l&#39;abbia già) nasce l&#39;esigenza di disporre di una history unificata per non correre il rischio che comandi digitati su una certa istanza non siano più disponibili per un riutilizzo futuro.&#xA;!--more--&#xA;In alcune configurazioni, le history si sincronizzano quando si chiude esplicitamente la sessione con &#34;exit&#34; o con CTRL-D ma il vantaggio vero sarebbe quello di disporre del merge da subito, senza aspettare di chiudere la sessione.&#xA;Anche perché, se per errore, l&#39;emulatore di terminale viene chiuso senza prima aver chiuso le sessioni (crash o anche terminando esplicitamente l&#39;applicazione se questo non coincide con il termine della sessione), le history non saranno più disponibili.&#xA;&#xA;Il merge delle history, in sè, è banale. C&#39;è bisogno di un pizzico di magia del comando history stesso e di un minimo di conoscenza della cosidetta &#34;shell&#34;.&#xA;&#xA;ubNota a margine:/b/u Su Gnu/Linux, tutto ciò è applicabile senza eccezioni. Su Mac OS uso bash 5 fornito da homebrew definendo un nome custom per il file history (HISTFILE in .bashrc) che altrimenti viene resettato impietosamente dopo non più di 500 linee, ogni tot riavvi e dopo tot tempo. Le ho provate tutte per andare oltre questo vincolo (sia modificando .bashrc e .profile sia usando alcune chicche specifiche del Mac che però hanno fallito tutte miseramente) e l&#39;unica cosa che ha funzionato è stato il nome custom.&#xA;2. La ricettina&#xA;&#xA;La shell&#xA;configurazione history&#xA;creazione script&#xA;&#xA;1. La shell (Spiegone iniziale)&#xA;&#xA;In un&#39;istanza di shell i comandi di uno script vengono solitamente eseguiti come sottoprocesso del processo genitore.&#xA;Uno stesso comando può essere invocato esternamente o essere implementato direttamente dalla shell (incorporato o builtin).&#xA;I comandi implementati nativamente possono essere più efficienti degli equivalenti esterni perché questi ultimi richiedono la generazione di un processo figlio per essere eseguiti. Inoltre un comando builtin può accedere a parti interne della shell.&#xA;D&#39;altro canto, l&#39;esecuzione di comandi esterni, col loro meccanismo di sottoprocesso, rende più semplice il ricorso ad un certo grado di parallelismo. Piuttosto che eseguire sequenzialmente n comandi interni, potrei decidere di ricorrere agli equivalenti esterni meno efficienti ma in grado di sfruttare il parallelismo dei sottoprocessi e lasciare al sistema operativo la gestione di questo aspetto.&#xA;&#xA;Il merge delle history viene ottenuto sfruttando questi concetti.&#xA;&#xA;Più facile a farsi che a dirsi:&#xA;&#xA;si scrive una funzione history (che sarà quella invocata dalla shell come se fosse un alias)&#xA;nel corpo della funzione history, si usa opportunamente il builtin history che effettuerà il merge vero e proprio&#xA;particolare attenzione verrà data all&#39;innesco di questa invocazione&#xA;&#xA;2. Configurazione history&#xA;&#xA;In ogni istanza di shell:&#xA;&#xA;si appende la sessione corrente al file history (history -a)&#xA;si cancella la history attualmente in sessione (history -c)&#xA;si ripristina la history in sessione dal file history (history -r)&#xA;si riscrive history come funzione in modo che possa essere invocata dall&#39;utente in maniera trasparente, eseguendo il sync e richiamando il builtin history (volendo scomodare impropriamente i design pattern, tutto ciò potrebbe ricordare l&#39;adapter per es.)&#xA;&#xA;Queste due funzioni andranno collocate in .bashrc (o .profile o .bash\profile, dipende dalla vostra configurazione) e richiamate attraverso il PROMPTCOMMAND in modo che, prima di ogni visualizzazione del prompt, si esegua il merge come volevamo&#xA;3. creazione script&#xA;&#xA;In .bashrc:&#xA;...&#xA;history(){&#xA;  syncHistory&#xA;  builtin history &#34;$@&#34;&#xA;}&#xA; &#xA;syncHistory(){&#xA;  builtin history -a&#xA;  HISTFILESIZE=$HISTFILESIZE&#xA;  builtin history -c&#xA;  builtin history -r&#xA;}&#xA;...&#xA;PROMPTCOMMAND=syncHistory&#xA;Appendice&#xA;&#xA;terminale vs. shell vs. emulatore di terminale&#xA;Quando ci si vuole sentire fighi e veri hacker, confessando con un falso senso di colpa e una strizzatina d&#39;occhio, &#34;di lavorare solo aprendo delle shell&#34;, commettiamo due piccole leggerezze:&#xA;&#xA;si dice una sciocchezza&#xA;se la contrazione intrinseca nella frase &#34;aprire una shell&#34; non è voluta, allora non si ha nessuna idea di quello che si sta facendo.&#xA;&#xA;La shell è un interprete di comandi (bash, dash, ksh, csh, zsh ecc.) e basta. Come può esserlo python per es. Mai sentito dire: &#34;ora apro un bel python&#34;, al di là del senso vagamente inquietante della frase.&#xA;Un terminale reale è una console collegata ad uno pseudo-dispositivo tty, collegato a sua volta fisicamente ad una porta seriale.&#xA;Un terminale virtuale in ambienti *nix è una console collegata ad uno pseudo-dispositivo tty virtuale (note anche come &#34;console tty&#34;) che permette di eseguire una shell all&#39;interno di una sessione X. Di solito accessibili con CTRL-ALT-Fn (per n=1..7 ma comunque configurabile).&#xA;Infine, quando si &#34;apre una shell&#34;, quello che si fa in realtà al 99,9999%, è aprire un emulatore di terminale (xterm, Gnome Terminal, Konsole, Iterm2, PuTTY, ecc.), ossia un terminale virtuale all&#39;interno di un ambiente grafico, su cui eseguire una shell come per i terminali reali/virtuali.&#xA;&#xA;Ulteriore suddivisione può essere operata considerando la tipologia di sessioni aperte e la possibilità o meno di disporre di un accesso al sistema.&#xA;&#xA;Il terminale virtuale esegue una shell interattiva con login (o shell di login). È ciò che avviene quando non si dispone di un login manager come KDM, GDM, LightDM ecc. che sono shell di login con GUI.&#xA;L&#39;emulatore di terminale permette di eseguire invece una shell interattiva senza login.&#xA;L&#39;esecuzione di uno script invece eseguirà una shell non interattiva.&#xA;&#xA;Quindi &#34;eseguo&#34; una shell, la &#34;uso&#34;. Ma non la &#34;apro&#34;, pur essendo una &#34;conchiglia&#34;.&#xA;&#xA;#bash #shell]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 27 gennaio 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/yTigNa1AvcvS/wAu9UAMi60jwJxOVuuQHAbPfOdmMBsQqST0NQMHC.jpg" alt="coding">
<small><i><a href="https://www.rawpixel.com/image/432709/free-photo-image-coding-cyber-software" rel="nofollow">Coding on a computer screen</a> by <a href="https://www.rawpixel.com/markusspiske" rel="nofollow">Markus Spiske</a> is licensed under <a href="https://creativecommons.org/publicdomain/zero/1.0/" rel="nofollow">CC-CC0 1.0</a></i></small></p>

<h2 id="1-introduzione">1. Introduzione</h2>

<p>Quando si eseguono varie istanze di shell bash (zsh mi pare l&#39;abbia già) nasce l&#39;esigenza di disporre di una <strong>history unificata</strong> per non correre il rischio che comandi digitati su una certa istanza non siano più disponibili per un riutilizzo futuro.

In alcune configurazioni, le history si sincronizzano quando si chiude esplicitamente la sessione con “exit” o con CTRL-D ma il vantaggio vero sarebbe quello di disporre del merge da subito, senza aspettare di chiudere la sessione.
Anche perché, se per errore, l&#39;emulatore di terminale viene chiuso senza prima aver chiuso le sessioni (crash o anche terminando esplicitamente l&#39;applicazione se questo non coincide con il termine della sessione), le history non saranno più disponibili.</p>

<p>Il merge delle history, in sè, è banale. C&#39;è bisogno di un pizzico di magia del comando <em>history</em> stesso e di un minimo di conoscenza della cosidetta “shell”.</p>

<p><u><b>Nota a margine:</b></u> Su Gnu/Linux, tutto ciò è applicabile senza eccezioni. Su Mac OS uso bash 5 fornito da homebrew definendo un nome custom per il file history (<em>HISTFILE</em> in .bashrc) che altrimenti viene resettato impietosamente dopo non più di 500 linee, ogni tot riavvi e dopo tot tempo. Le ho provate tutte per andare oltre questo vincolo (sia modificando .bashrc e .profile sia usando alcune chicche specifiche del Mac che però hanno fallito tutte miseramente) e l&#39;unica cosa che ha funzionato è stato il nome custom.</p>

<h2 id="2-la-ricettina">2. La ricettina</h2>
<ol><li>La shell</li>
<li>configurazione history</li>
<li>creazione script</li></ol>

<h3 id="1-la-shell-spiegone-iniziale">1. La shell (Spiegone iniziale)</h3>

<p>In un&#39;istanza di shell i comandi di uno script vengono solitamente eseguiti come sottoprocesso del processo genitore.
Uno stesso comando può essere invocato esternamente o essere implementato direttamente dalla shell (incorporato o <strong>builtin</strong>).
I comandi implementati nativamente possono essere più efficienti degli equivalenti esterni perché questi ultimi richiedono la generazione di un processo figlio per essere eseguiti. Inoltre un comando builtin può accedere a parti interne della shell.
D&#39;altro canto, l&#39;esecuzione di comandi esterni, col loro meccanismo di sottoprocesso, rende più semplice il ricorso ad un certo grado di parallelismo. Piuttosto che eseguire sequenzialmente <em>n</em> comandi interni, potrei decidere di ricorrere agli equivalenti esterni meno efficienti ma in grado di sfruttare il parallelismo dei sottoprocessi e lasciare al sistema operativo la gestione di questo aspetto.</p>

<p>Il merge delle history viene ottenuto sfruttando questi concetti.</p>

<p>Più facile a farsi che a dirsi:</p>
<ol><li>si scrive una funzione <em>history</em> (che sarà quella invocata dalla shell come se fosse un alias)</li>
<li>nel corpo della funzione <em>history</em>, si usa opportunamente il builtin <strong>history</strong> che effettuerà il merge vero e proprio</li>
<li>particolare attenzione verrà data all&#39;innesco di questa invocazione</li></ol>

<h3 id="2-configurazione-history">2. Configurazione history</h3>

<p>In ogni istanza di shell:</p>
<ol><li>si appende la sessione corrente al file history (<code>history -a</code>)</li>
<li>si cancella la history attualmente in sessione (<code>history -c</code>)</li>
<li>si ripristina la history in sessione dal file history (<code>history -r</code>)</li>
<li>si riscrive <code>history</code> come funzione in modo che possa essere invocata dall&#39;utente in maniera trasparente, eseguendo il sync e richiamando il builtin <em>history</em> (volendo scomodare impropriamente i design pattern, tutto ciò potrebbe ricordare l&#39;adapter per es.)</li></ol>

<p>Queste due funzioni andranno collocate in <em>.bashrc</em> (o <em>.profile</em> o <em>.bash_profile</em>, dipende dalla vostra configurazione) e richiamate attraverso il PROMPT_COMMAND in modo che, prima di ogni visualizzazione del prompt, si esegua il merge come volevamo</p>

<h3 id="3-creazione-script">3. creazione script</h3>

<p>In .bashrc:</p>

<pre><code class="language-bash">...
history(){
  syncHistory
  builtin history &#34;$@&#34;
}
 
syncHistory(){
  builtin history -a
  HISTFILESIZE=$HISTFILESIZE
  builtin history -c
  builtin history -r
}
...
PROMPT_COMMAND=syncHistory
</code></pre>

<h2 id="appendice">Appendice</h2>

<p><strong>terminale vs. shell vs. emulatore di terminale</strong>
Quando ci si vuole sentire fighi e veri hacker, confessando con un falso senso di colpa e una strizzatina d&#39;occhio, “di lavorare solo aprendo delle shell”, commettiamo due piccole leggerezze:</p>
<ul><li>si dice una sciocchezza</li>
<li>se la contrazione intrinseca nella frase “aprire una shell” non è voluta, allora non si ha nessuna idea di quello che si sta facendo.</li></ul>
<ol><li>La shell è <strong>un interprete di comandi</strong> (bash, dash, ksh, csh, zsh ecc.) e basta. Come può esserlo python per es. Mai sentito dire: “ora apro un bel python”, al di là del senso vagamente inquietante della frase.</li>
<li>Un <strong>terminale reale</strong> è una console collegata ad uno pseudo-dispositivo tty, collegato a sua volta fisicamente ad una porta seriale.</li>
<li>Un <strong>terminale virtuale</strong> in ambienti *nix è una console collegata ad uno pseudo-dispositivo tty <strong>virtuale</strong> (note anche come “console tty”) che permette di <strong>eseguire</strong> una shell all&#39;interno di una sessione X. Di solito accessibili con CTRL-ALT-Fn (per n=1..7 ma comunque configurabile).</li>
<li>Infine, quando si “apre una shell”, quello che si fa in realtà al 99,9999%, è aprire <strong>un emulatore di terminale</strong> (xterm, Gnome Terminal, Konsole, Iterm2, PuTTY, ecc.), ossia un <strong>terminale virtuale all&#39;interno di un ambiente grafico</strong>, su cui eseguire una shell come per i terminali reali/virtuali.</li></ol>

<p>Ulteriore suddivisione può essere operata considerando la tipologia di sessioni aperte e la possibilità o meno di disporre di un accesso al sistema.</p>
<ul><li>Il terminale virtuale esegue una <strong>shell interattiva con login (o shell di login)</strong>. È ciò che avviene quando non si dispone di un login manager come KDM, GDM, LightDM ecc. che sono shell di login con GUI.</li>
<li>L&#39;emulatore di terminale permette di eseguire invece una <strong>shell interattiva senza login</strong>.</li>
<li>L&#39;esecuzione di uno script invece eseguirà una <strong>shell non interattiva.</strong></li></ul>

<p>Quindi “eseguo” una shell, la “uso”. Ma non la “apro”, pur essendo una “conchiglia”.</p>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:shell" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">shell</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/merge-delle-history-in-tempo-reale</guid>
      <pubDate>Mon, 27 Feb 2023 22:37:09 +0000</pubDate>
    </item>
  </channel>
</rss>