Riesci a risolvere questo Bash Script Puzzle?

Benvenuti alla Bash Challenge # 7 di Yes I Know IT & It's FOSS. In questa sfida settimanale ti mostreremo una schermata del terminale e conteremo su di te per aiutarci ad ottenere il risultato che volevamo. Ci possono essere molte soluzioni e essere creativi è la parte più divertente della sfida.

Se non lo hai già fatto, dai un'occhiata alle sfide precedenti:

  • Bash Challenge 6
  • Bash Challenge 5

Puoi anche comprare queste sfide (con sfide non pubblicate) in forma di libro e supportarci:

Pronto a giocare ? Quindi ecco la sfida di questa settimana.

Il contatore di token

Questa settimana torneremo a una sfida più "orientata alla programmazione". La descrizione è un po 'astratta, prova a stare con me per pochi minuti e spero che la descrizione qui sotto sia abbastanza chiara:

Ho un flusso di token 'ROSSO' o 'BLU'. Se lo desideri, puoi considerarlo come una rappresentazione di un flusso di eventi, ad esempio. Non ho alcun controllo particolare su quel flusso. So solo che produce l'uno o l'altro token, imprevedibilmente. E so che il vapore è finito (cioè: ad un certo punto, non ci saranno più dati da leggere).

Per il gusto di questa sfida, ho usato una funzione Bash per produrre quel flusso. Non sei autorizzato a cambiarlo in alcun modo.

# You MUST NOT change that : stream() { TOKENS=( "RED" "BLUE" ) for((i=0;i<100;++i)) ; do echo ${TOKENS[RANDOM%2]} done } 

Il mio obiettivo è contare sia il numero di gettoni ROSSO e BLU che c'era nel flusso. Da solo, sono riuscito a trovare una soluzione per contare il numero di token RED da solo:

  # You MUST change that stream | \ grep -F RED | wc -l > RED.CNT cat RED.CNT 

Sfortunatamente, non sono riuscito a trovare alcuna soluzione per contare sia i token ROSSI che quelli BLU. Ecco perché ho bisogno del tuo aiuto. Qualche idea ?

Non vediamo l'ora di leggere le tue soluzioni nella sezione commenti qui sotto!

Pochi dettagli

Per creare questa sfida, ho usato:

  • GNU Bash, versione 4.4.5 (x86_64-pc-linux-gnu)

  • Debian 4.8.7-1 (amd64)
  • Tutti i comandi sono quelli forniti con una distribuzione Debian standard
  • Nessun comando era alias

La soluzione

Come si riproduce

Ecco il codice raw che abbiamo usato per produrre questa sfida. Se lo esegui in un terminale, sarai in grado di riprodurre esattamente lo stesso risultato visualizzato nell'illustrazione della sfida (supponendo che tu stia utilizzando la stessa versione del software di me):

 rm -rf ItsFOSS mkdir -p ItsFOSS cd ItsFOSS clear stream() { TOKENS=( "RED" "BLUE" ) for((i=0;i RED.CNT cat RED.CNT 

Qual'era il problema ?

L'unica difficoltà qui è stata il mio tentativo iniziale è scartare una parte dell'input, perché invio direttamente il flusso di dati al grep .

Fondamentalmente ci sono tre approcci per risolvere questo problema:

  • Memorizza i dati del flusso ed elaborali in seguito;

  • Duplica lo stream ed elabora due percorsi indipendenti per i token RED e BLUE;
  • Gestisci entrambi i casi nello stesso comando in cui arrivano.

Per quello che vale, dopo ogni soluzione, fornisco l'utilizzo in tempo reale osservato sul mio sistema. Questa è solo un'indicazione e deve essere presa con cautela. Quindi sentiti libero di fare da solo il confronto!

L'approccio al negozio e al processo

La più semplice implementazione dell'approccio store-and-process è ovvia:

 stream > stream.cache grep -F RED RED.CNT grep -F BLUE BLUE.CNT rm stream.cache (1.3s for 10, 000, 000 tokens) 

Funziona, ma ha diversi svantaggi: devi archiviare i dati e i dati vengono elaborati sequenzialmente per ciascun token. Più sottile, leggendo due volte il file stream.cache, è possibile che si stream.cache qualche condizione di competizione se un processo concorrente aggiorna il file durante l'elaborazione.

Sempre nella categoria store-and-process, ecco una soluzione completamente diversa:

 stream | sort | uniq -c (5.9s for 10, 000, 000 tokens) 

Considero un approccio store-and-process, dal momento che il comando sort deve prima leggere e memorizzare (in RAM o su disco) tutti i dati prima di poterli elaborare. Più precisamente, sul mio sistema Debian, il comando sort crea diversi file temporanei in /tmp con permessi rw . Fondamentalmente questa soluzione ha gli stessi inconvenienti del primo ma con prestazioni molto peggiori.

Flusso duplicato

Dobbiamo davvero / memorizzare / i dati / prima / elaborarli? No. Un'idea molto più intelligente sarebbe quella di suddividere il flusso in due parti, elaborando un tipo di token in ogni flusso secondario:

 stream | tee >(grep -F RED | wc -l > RED.CNT) \ >(grep -F BLUE | wc -l > BLUE.CNT) \ > /dev/null (0.8s for 10, 000, 000) 

Qui, non ci sono file intermedi. Il comando tee replica i dati del flusso non appena arrivano. Ogni unità di elaborazione ottiene la propria copia dei dati e li può elaborare al volo.

Questa è un'idea intelligente perché non solo gestiamo i dati non appena arrivano, ma ora abbiamo l'elaborazione parallela .

Gestisci i dati non appena arrivano

In informatica, probabilmente diremmo che la soluzione precedente ha adottato un approccio funzionale al problema. D'altra parte, le prossime saranno soluzioni puramente imperative. Qui, leggeremo ciascun token e / se / questo è un token RED / / / / incrementeremo un contatore RED, altrimenti se / this è un token BLU, incrementeremo un contatore BLUE.

Questa è una semplice implementazione di Bash di questa idea:

 declare -i RED=0 BLUE=0 stream | while read TOKEN; do case "$TOKEN" in RED) RED+=1 ;; BLUE) BLUE+=1 ;; esac done (103.2s for 10, 000, 000 tokens) 

Infine, essendo un grande fan del comando AWK, non resisterò alla tentazione di usarlo per risolvere quella sfida in modo pulito ed elegante:

 stream | awk ' /RED/ { RED++ } /BLUE/ { BLUE++ } END { printf "%5d %5d\n", RED, BLUE } ' (2.6s for 10, 000, 000 tokens) 

Il mio programma AWK è composto da tre regole:

  • Quando incontri una linea che contiene la parola ROSSO, aumenta ( ++ ) il contatore ROSSO

  • Quando incontri una linea che contiene la parola BLU, aumenta il contatore BLU
  • Alla fine dell'ingresso, visualizzare entrambi i contatori.

Naturalmente per comprendere appieno che devi sapere, ai fini degli operatori matematici, si presume che le variabili AWK non inizializzate siano pari a zero.

Funziona alla grande. Ma richiede la duplicazione della stessa regola per ogni token. Non è un grosso problema qui perché abbiamo solo due diversi token. Più fastidioso se ne abbiamo molti di loro. Per risolvere ciò, potremmo fare affidamento sugli array :

 stream | awk ' { C[$0]++ } END { printf "%5d %5d\n", C["RED"], C["BLUE"] } ' (2.0s for 10, 000, 000 tokens) 

Abbiamo solo bisogno di due regole qui, qualunque sia il numero di token:

  • Qualunque sia il token letto ( $0 ) aumenta la cella matrice corrispondente (qui, C["RED"] o C["BLUE"]

  • Alla fine dell'ingresso, visualizzare il contenuto dell'array sia per le celle "RED" che "BLUE" .

Notare che "RED" e "BLUE" sono ora stringhe di caratteri (hai visto le doppie virgolette attorno a loro?) E questo non è un problema per AWK poiché supporta gli array associativi. E proprio come le semplici variabili, si presume che le celle non inizializzate in un array associativo AWK siano zero per gli operatori matematici.

Come ho spiegato prima, ho fatto la scelta di usare AWK qui. Ma i fan di Perl potrebbero avere un'opinione diversa sull'argomento. Se sei uno di loro, perché non pubblicare la tua soluzione nella sezione dei commenti?

Ad ogni modo, speriamo che questa sfida ti sia piaciuta. E rimani sintonizzato per divertirti di più!

Raccomandato

Come guardare le distribuzioni basate su Linux su Hulu On Arch
2019
Xenlism WildFire: Minimal Icon Theme per desktop Linux
2019
Midori: un browser Web open source leggero
2019