Можеш ли да решиш тази Bash Script Puzzle?

Добре дошли в Bash Challenge # 7 от Да знам IT & FOSS. В това седмично предизвикателство ще ви покажем един терминален екран и ще разчитаме на вас, за да ни помогнете да постигнем желания резултат. Може да има много решения, а творчеството е най-забавната част от предизвикателството.

Ако вече не сте го направили, разгледайте предишните предизвикателства:

  • Bash Challenge 6
  • Bash Challenge 5

Можете също така да закупите тези предизвикателства (с непубликувани предизвикателства) под формата на книга и да ни подкрепите:

Готов за игра ? Така че това е предизвикателството тази седмица.

Броячът на символите

Тази седмица се връщаме към едно по-ориентирано към програмирането предизвикателство. Описанието е малко абстрактно, опитайте се да останете с мен за няколко минути - и се надявам описанието по-долу да е достатъчно ясно:

Имам поток от символи „RED“ или „BLUE“. Ако искате, можете да го приемете като представяне на поток от събития, например. Нямам особен контрол върху този поток. Просто знам, че произвежда един или друг знак, непредсказуемо. И знам, че парата е крайна (т.е.: в даден момент няма да има повече данни за четене).

В името на това предизвикателство използвах Bash функция, за да произведа този поток. Не можете да го променяте.

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

Целта ми е да преброя и двата броя ЧЕРВЕНИ и СИНОВИ токени в потока. Аз самият успях да намеря решение, за да преброя само броя RED символи:

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

За съжаление, не можах да намеря никакво решение да преброя двата червени и сини символа. Затова имам нужда от вашата помощ. Някаква идея ?

Очакваме с нетърпение да прочетете вашите решения в раздела за коментари по-долу!

Малко подробности

За да създам това предизвикателство, използвах:

  • GNU Bash, версия 4.4.5 (x86_64-pc-linux-gnu)

  • Debian 4.8.7-1 (amd64)
  • Всички команди са тези, които се доставят със стандартна дистрибуция на Debian
  • Команди не са били псевдоним

Решението

Как да се възпроизвежда

Това е суровият код, който използвахме, за да произведем това предизвикателство. Ако изпълните това в терминал, ще можете да възпроизведете точно същия резултат, както е показано в илюстрацията на предизвикателството (ако използвате същата софтуерна версия като мен):

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

Какъв беше проблема ?

Единствената трудност тук беше първоначалният ми опит да отхвърля част от входа, защото директно изпращам потока от данни към grep .

По принцип има три подхода за решаване на този проблем:

  • Съхранявайте поточните данни и ги обработвайте след това;

  • Дублирайте потока и обработете два независими пътя за символи RED и BLUE;
  • Управлявайте и двата случая в същата команда, когато пристигнат.

За какво си струва, след всяко решение, давам наблюдение в реално време на моята система. Това е само индикация и трябва да се приема с повишено внимание. Така че не се колебайте сами да направите сравнението си!

Подходът на магазина и процеса

Най-простото прилагане на подхода за съхранение и процес е очевидно:

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

Той работи, но има няколко недостатъка: трябва да съхранявате данните и данните се обработват последователно за всеки токен. По-фините, когато четете два пъти файла stream.cache, потенциално имате някакво състезателно условие, ако едновременно процесът обновява този файл по време на обработка.

Все още в категорията "съхранявайте и обработвайте" е съвсем различно решение:

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

Считам, че подходът за съхранение и процес, тъй sort командата за sort трябва първо да прочете и съхрани (или в RAM или на диск) всички данни, преди да може да ги обработи. По-точно, в моята Debian система командата sort създава няколко временни файла в /tmp с rw разрешения. По принцип това решение има същите недостатъци като първото, но с много по-лоши показатели.

Дублиран поток

Наистина ли трябва да съхраняваме данните / преди тях / да ги обработваме? Не. Много по-умна идея би било да се раздели потока на две части, като се обработи един вид токен във всеки под-поток:

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

Тук няма междинни файлове. Командата tee репликира поточните данни, когато те пристигнат. Всяко обработващо устройство получава собствено копие на данните и може да ги обработва в движение.

Това е умна идея, защото не само ние обработваме данните, тъй като те пристигат, но сега имаме паралелна обработка.

Обработвайте данните при пристигането им

В компютърните науки вероятно бихме казали, че предишното решение е използвало функционален подход към проблема. От друга страна, следващите ще бъдат чисто императивни решения. Тук ще четем всеки токен, и / ако / това е ЧЕРВЕНЕН знак, / тогава / ще увеличим ЧЕРВЕН брояч, / иначе ако / това е СИНЕН знак, ще увеличим СИНИЯТ брояч.

Това е обикновена Bash изпълнение на тази идея:

 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) 

И накрая, като голям фен на командата AWK, няма да се противопоставя на изкушението да го използвам, за да решат това предизвикателство по чист и елегантен начин:

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

Моята програма AWK се състои от три правила:

  • Когато срещнете ред, съдържащ думата RED, увеличете ( ++ ) червения брояч

  • Когато срещнете линия, съдържаща думата BLUE, увеличете BLUE брояча
  • На края на входа се показват двата брояча.

Разбира се, за да разберем напълно, че трябва да знаете, за целите на математическите оператори, неинициализираните AWK променливи се приемат за нула.

Това работи чудесно. Но това изисква дублиране на едно и също правило за всеки знак. Не е голяма работа, тъй като имаме само два различни символа. Още по-досадно, ако имаме много от тях. За да решим това, можем да разчитаме на масиви :

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

Тук са ни необходими само две правила, независимо от броя на токените:

  • Каквото и да е прочетеният маркер ( $0 ), увеличава съответната клетка от масив (тук или C["RED"] или C["BLUE"] )

  • На END на входа, покажете съдържанието на масива както за "RED" и за "BLUE" клетки.

Моля, обърнете внимание, че "RED" и "BLUE" са вече символни низове (видяхте ли двойните кавички около тях?) И това не е проблем за AWK тъй като поддържа асоциативни масиви. И точно както обикновените променливи, неинициализираните клетки в асоциативния масив AWK се приемат за нула за математическите оператори.

Както обясних преди, направих избора да използвам AWK тук. Но феновете на Perl може да имат различно мнение по темата. Ако сте един от тях, защо да не публикувате собствено решение в раздела за коментари?

Както и да е, ние се надяваме, че ви харесва това предизвикателство. И останете на линия за по-забавно!

Препоръчано

Пълно ръководство за трикратно зареждане на Windows, Kubuntu и Debian
2019
GNOME 3.26 Издаден! Проверете новите функции
2019
3D отпечатване с отворен код: проучване на научни и медицински решения
2019