Скрипт, который продолжает читать поток

Вопрос или проблема

Иногда я cat поток, подобный /dev/input/event0.

Я хочу написать скрипт, который будет делать что-то каждый раз, когда появляется новый вывод.

Определение нового вывода может быть таким: каждый раз, когда читается байт.

Как это можно сделать? Есть ли какая-то команда, которая это делает?

В оболочке вы будете ограничены полными строками. Вам придется использовать C/Perl/Python/что-то еще для более точного чтения.

while read line; do
  # делать что-то на основе содержимого $line; не забудьте экранировать его
done </dev/input/event0

Вариант ответа geekosaur:
Вы можете попробовать read -n 1 byte, чтобы читать байт за байтом, а затем что-то делать с $byte.

ИЗМЕНЕНИЕ:

Я только что попробовал это, так как никогда не использовал эту команду раньше (просто посмотрел info bash), но, похоже, она игнорирует все пробелы и окончание строк. У меня пока нет объяснения для этого.

Попробуйте следующие скрипты, чтобы уточнить аргументы команд:

(for j in $(seq 1 10); do for i in $(seq 1 100); do echo -n "$i, "; sleep .02; done; echo "& $j."; done) | (while read line; do echo $line; done)

(for j in $(seq 1 10); do for i in $(seq 1 100); do echo -n "$i, "; sleep .05; done; echo "& $j."; done) | (while read -n 1 byte; do echo -n "$byte"; done)

К сожалению, это не дает ожидаемого результата.

ИЗМЕНЕНИЕ (с помощью Криса):

(for j in $(seq 1 10); do for i in $(seq 1 100); do echo -n "$i, "; sleep .02; done; echo "& $j."; done) | (while IFS= read -N 1 byte; do echo -n "$byte"; done)

Это дает именно ожидаемый результат.
Примечание: использование -n, -N или -rN не изменяет результат, все работает (с текстом, я не проверял ограничение, о котором говорит Крис: 0x00 и 0xff).

Я осознаю, что этот ответ дан через 13 лет после вопроса и работает только с более поздними версиями Bash (по крайней мере, 4.x, вероятно, даже 5.x), но я считаю, что он все еще квалифицируется как “ответ”, а не просто комментарий – в значительной степени потому, что теперь есть способ надежно читать произвольные двоичные данные из потока в чистом Bash. Однако он не очень производителен, поскольку следует читать по одному байту за раз, но, если все сделано правильно, это все еще может быть “достаточно хорошим” для большинства случаев с выбросами до нескольких килобайтов/мегабайтов.

Одно из неизбежных архитектурных ограничений заключается в том, что Bash (как и большинство инструментов Unix) прагматически спроектирован в рамках некоторых идиом C из-за того, что он построен на C. Одной из явных особенностей являются строки, завершенные нулем. Поэтому следующий “глитч” становится неудивительным и подчеркивает, что, хотя нулевые байты могут передаваться между исполняемыми файлами (и генерироваться для потоков из шаблонов с помощью инструментов, таких как printf), любое прямое управление нулем внутри самого Bash должно выполняться через методы “генератора” или “побочного эффекта” и не встраиваться наивно в строки (строка просто будет считаться обрезанной на первом нуле, который интерпретируется как “конец строки” стандартным обработчиком строк C).

$ # Попытка встроить фактический ноль в строку и вывести это (не работает)
$ echo $'abc\ndef\0ghi'
abc
def
$ echo $'abc\ndef\0gh' | xxd
00000000: 6162 630a 6465 660a                      abc.def.
$ # Попытка сгенерировать ноль на лету с помощью обозначения шаблона "\0" для printf
$ # (работает) 
$ printf 'abc\ndef\0ghi\n'
abc
defghi
$ printf 'abc\ndef\0ghi\n' | xxd
00000000: 6162 630a 6465 6600 6768 690a            abc.def.ghi.

Обходные трюки уже могли ранее применяться с использованием эвристических замен или внешних исполняемых файлов и т. д. – что убивает производительность и надежность (особенно при использовании подсистем, строковых типов ввода-вывода и т. д.). Теперь возможный трюк разбора заключается в том, что, когда вы читаете строго по одному байту (а не потенциально многобайтовый символ) за раз, если вы кажетесь “успешно прочитавшим пустую строку”, вы знаете, что прочитали нулевой байт. Следующая проблема возникает, когда используется любая многобайтовая/переменной байтовая кодировка (что очень часто имеет место с сейчас вошедшим в моду UTF-8), все инструменты, которые читают символ за раз вместо байта за раз (такие как read и printf), будут делать неверные предположения и трансформации. Это можно обойти, заставив использовать кодировку с одним байтом (или просто C) локаль, например, с LANG=C. Для встроенных функций, таких как read, также необходимо обойти другие контрпродуктивные предположения (например, отключить разбор разделителей полей ввода, используя IFS=, избежать интерпретации escape-кодов, используя -r, читать до каждого нуля вместо конца строки, используя -d '', читать строго один символ за раз, используя -N 1 и т. д.).

Некоторое время назад я написал утилиты read_stream и write_stream (которые используют вышеупомянутые свойства для чтения и обратного процесса для записи) и ради этого ответа я просто объединил их в универсальную функцию stream (см. ниже). Она может читать/писать из/в STDIN каждый 8-битный байт между 0 и 255 как числовой код символа, одновременно избегая “захвата стандартного вывода подсистем” или “чтения из подстановок процессов” (что действительно убило бы производительность):

function stream {
  declare -r action="${1}"       # читать | писать
  declare -rn bytes_array="${2}" # "declare -ai"
  declare -rn index="${3}"       # "declare -i"
  declare -ri limit="${4:--1}"
  declare char template
  declare -i end overrun
  if ((limit == -1)); then
    end=-1
    overrun=1
  else
    ((end = begin + limit)) ||
      true
    overrun=0
  fi
  if [[ "${action}" == 'read' ]]; then
    while LANG=C IFS= read -r -d '' -n 1 'char'; do
      # Это даже работает для пустого $char (ноль), потому что одинарная кавычка
      # сама по себе переводится как "0" байт.
      LANG=C printf -v 'bytes_array[index++]' '%d' "'${char}"
      if ((index == end)); then
        overrun=1
        break
      fi
    done
  else # if [[ "${action}" == 'write' ]]; then
    if ((end == -1)); then
      printf -v 'template' '\\x%02x' "${bytes_array[@]:index}"
    else
      printf -v 'template' '\\x%02x' "${bytes_array[@]:index:limit}"
      [[ -v 'bytes_array[index + limit - 1]' ]] ||
        overrun=1
    fi
    printf "${template}"
  fi
  ((end == -1 || overrun == 1)) ||
    return 1
}
function stream_test {
  # Получает ниже приведенный генерируемый поток сырых байтов как целые числа, группами по 10 
  declare -ai out_data=()
  declare -i out_data_index=0
  while true; do
    if stream 'read' 'out_data' 'out_data_index' 10; then
      declare -p 'out_data'
      out_data_index=0
    else
      if ((${#out_data_index} != 0)); then
        out_data=("${out_data[@]:0:out_data_index}")
        declare -p 'out_data'
      fi
      break
    fi
  done < <(
    # Генерирует поток сырых байтов из массива, содержащего целые числа от 0 до 255
    declare -ai in_data=({0..255})
    declare -i in_data_index=0
    stream 'write' 'in_data' 'in_data_index'
  )
}

Запуск теста:

$ time stream_test
declare -ai out_data=([0]="0" [1]="1" [2]="2" [3]="3" [4]="4" [5]="5" [6]="6" [7]="7" [8]="8" [9]="9")
declare -ai out_data=([0]="10" [1]="11" [2]="12" [3]="13" [4]="14" [5]="15" [6]="16" [7]="17" [8]="18" [9]="19")
declare -ai out_data=([0]="20" [1]="21" [2]="22" [3]="23" [4]="24" [5]="25" [6]="26" [7]="27" [8]="28" [9]="29")
declare -ai out_data=([0]="30" [1]="31" [2]="32" [3]="33" [4]="34" [5]="35" [6]="36" [7]="37" [8]="38" [9]="39")
declare -ai out_data=([0]="40" [1]="41" [2]="42" [3]="43" [4]="44" [5]="45" [6]="46" [7]="47" [8]="48" [9]="49")
declare -ai out_data=([0]="50" [1]="51" [2]="52" [3]="53" [4]="54" [5]="55" [6]="56" [7]="57" [8]="58" [9]="59")
declare -ai out_data=([0]="60" [1]="61" [2]="62" [3]="63" [4]="64" [5]="65" [6]="66" [7]="67" [8]="68" [9]="69")
declare -ai out_data=([0]="70" [1]="71" [2]="72" [3]="73" [4]="74" [5]="75" [6]="76" [7]="77" [8]="78" [9]="79")
declare -ai out_data=([0]="80" [1]="81" [2]="82" [3]="83" [4]="84" [5]="85" [6]="86" [7]="87" [8]="88" [9]="89")
declare -ai out_data=([0]="90" [1]="91" [2]="92" [3]="93" [4]="94" [5]="95" [6]="96" [7]="97" [8]="98" [9]="99")
declare -ai out_data=([0]="100" [1]="101" [2]="102" [3]="103" [4]="104" [5]="105" [6]="106" [7]="107" [8]="108" [9]="109")
declare -ai out_data=([0]="110" [1]="111" [2]="112" [3]="113" [4]="114" [5]="115" [6]="116" [7]="117" [8]="118" [9]="119")
declare -ai out_data=([0]="120" [1]="121" [2]="122" [3]="123" [4]="124" [5]="125" [6]="126" [7]="127" [8]="128" [9]="129")
declare -ai out_data=([0]="130" [1]="131" [2]="132" [3]="133" [4]="134" [5]="135" [6]="136" [7]="137" [8]="138" [9]="139")
declare -ai out_data=([0]="140" [1]="141" [2]="142" [3]="143" [4]="144" [5]="145" [6]="146" [7]="147" [8]="148" [9]="149")
declare -ai out_data=([0]="150" [1]="151" [2]="152" [3]="153" [4]="154" [5]="155" [6]="156" [7]="157" [8]="158" [9]="159")
declare -ai out_data=([0]="160" [1]="161" [2]="162" [3]="163" [4]="164" [5]="165" [6]="166" [7]="167" [8]="168" [9]="169")
declare -ai out_data=([0]="170" [1]="171" [2]="172" [3]="173" [4]="174" [5]="175" [6]="176" [7]="177" [8]="178" [9]="179")
declare -ai out_data=([0]="180" [1]="181" [2]="182" [3]="183" [4]="184" [5]="185" [6]="186" [7]="187" [8]="188" [9]="189")
declare -ai out_data=([0]="190" [1]="191" [2]="192" [3]="193" [4]="194" [5]="195" [6]="196" [7]="197" [8]="198" [9]="199")
declare -ai out_data=([0]="200" [1]="201" [2]="202" [3]="203" [4]="204" [5]="205" [6]="206" [7]="207" [8]="208" [9]="209")
declare -ai out_data=([0]="210" [1]="211" [2]="212" [3]="213" [4]="214" [5]="215" [6]="216" [7]="217" [8]="218" [9]="219")
declare -ai out_data=([0]="220" [1]="221" [2]="222" [3]="223" [4]="224" [5]="225" [6]="226" [7]="227" [8]="228" [9]="229")
declare -ai out_data=([0]="230" [1]="231" [2]="232" [3]="233" [4]="234" [5]="235" [6]="236" [7]="237" [8]="238" [9]="239")
declare -ai out_data=([0]="240" [1]="241" [2]="242" [3]="243" [4]="244" [5]="245" [6]="246" [7]="247" [8]="248" [9]="249")
declare -ai out_data=([0]="250" [1]="251" [2]="252" [3]="253" [4]="254" [5]="255")

real    0m0.029s
user    0m0.025s
sys 0m0.005s

Постобработка чтения обычно будет либо:

  1. С помощью трюка с printf в 2 шага для генерации потока из кодов, как используется в части писателя stream.

  2. С помощью таблицы поиска символов (даже индексированный массив подойдет, нет необходимости в ассоциативном массиве, просто поиск по целому числу). См. ниже функцию get_random_chars (которую я написал), которая это делает. Она берет произвольные байты из /dev/urandom (используя функцию stream) и отображает их по таблице поиска символов, используя деление по модулю. Поскольку никакие байты (даже нули) не отфильтровываются из входных данных urandom и поскольку 64 элемента делятся на 256 возможных байтов (от 0 до 255) ровно 4 раза, нет риска “веса” или “пристрастия” к случайности результатов.

В качестве бонуса часть чтения stream демонстрирует чтение произвольными размерами блоков за раз в цикле (и обработку завершающего частичного блока), как альтернатива чтению по одной строке за раз.

function get_random_chars {
  declare -rn out="${1}"
  declare -ri len="${2:-10}"
  declare -ra chars=(
    _ .
    0 1 2 3 4 5 6 7 8 9
    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
    a b c d e f g h i j k l m n o p q r s t u v w x y z
  )
  declare -ai bytes=()
  declare -ri chars_num=${#chars[@]}
  declare -i bytes_index=0 iter
  declare string=''
  stream 'read' 'bytes' 'bytes_index' "${len}" <'/dev/urandom' ||
    return 1
  for ((iter = 0; iter < len; iter++)); do
    string+="${chars[$((bytes[iter] % chars_num))]}"
  done
  out="${string}"
}
function get_random_chars_test {
  declare password
  get_random_chars 'password' 32
  printf 'password: %s\n' "${password}"
}

Запуск теста:

$ time get_random_chars_test
password: GTiLykIKqyDXu9e_XYSEfVDt74Ei_auO

real    0m0.007s
user    0m0.003s
sys 0m0.004s

Ответ или решение

Чтобы создать скрипт, который будет считывать данные из потока в реальном времени и выполнять определённые действия по мере поступления новых байтов, мы можем использовать различные языки программирования. В этом ответе мы сосредоточимся на использовании Bash, а также опишем некоторые нюансы, связанные с считыванием двоичных данных.

Общая схема работы

Во-первых, важно понять, что считывание данных из устройства (например, /dev/input/event0) может происходить как построчно с помощью команды read, так и побайтово. Однако Bash имеет определённые ограничения при работе с байтами, вводя сложности при непосредственной обработке двоичных данных. Для управления вводом и выводом данных на более низком уровне, что особенно важно для двоичных потоков, лучше всего подойдёт чтение байт по одному:

#!/bin/bash

while IFS= read -r -n1 byte; do
  # Обработка каждого считанного байта, например:
  printf '%d\n' "'$byte"  # Вывод значения байта в десятичной системе
done < /dev/input/event0

Параметры команды read

  • -n1: позволяет прочитать только один байт.
  • IFS=: устанавливает разделитель для ввода в пустую строку, чтобы избежать разделения по пробелам.

Ключевые моменты при работе с байтовыми потоками

  1. Обработка нулевых байтов: При работе с двоичными данными нужно понимать, что встреча нулевого байта (0x00) может привести к несоответствиям, поскольку многие инструменты Unix обрабатывают строки как null-терминированные. Чтение в Bash должно учитывать этот нюанс.

  2. Чтение и вывод в двоичном формате: Для вывода байтов в шестнадцатеричном формате, который часто более удобен для диагностики, можно использовать xxd или hexdump.

Расширенный пример обработки

Для обработки байтов более эффективно, можно написать функцию, которая будет считывать поток и сохранять байты в массив:

function read_stream {
  declare -a bytes=()
  declare -i index=0

  while IFS= read -r -n1 byte; do
    bytes[index++]=$(printf '%d' "'$byte")
  done < /dev/input/event0

  echo "${bytes[@]}"  # Вывод всех считанных байтов
}

Оптимизация производительности

Чтение по одному байту не является самой эффективной стратегией, особенно при большом количестве данных. Для повышения производительности, можно рассмотреть использование Python или Perl, которые работают более эффективно с потоками и двоичными данными.

Пример на Python

import sys

def read_stream():
    while True:
        byte = sys.stdin.read(1)
        if not byte:  # Конец потока
            break
        print(ord(byte))  # Вывод значения байта в десятичной системе

if __name__ == "__main__":
    read_stream()

Заключение

Создание скрипта для обработки потока данных требует понимания особенностей работы с байтовыми данными и выбора подходящего инструмента. Bash предоставляет возможности для простых задач, однако в некоторых случаях может потребоваться использование более мощных языков программирования, таких как Python, для эффективной обработки данных. Важно учитывать различия в том, как строки и байты обрабатываются в разных языках, чтобы избежать неожиданных ошибок.

Оцените материал
Добавить комментарий

Капча загружается...