Вопрос или проблема
Иногда я 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
Постобработка чтения обычно будет либо:
-
С помощью трюка с
printf
в 2 шага для генерации потока из кодов, как используется в части писателяstream
. -
С помощью таблицы поиска символов (даже индексированный массив подойдет, нет необходимости в ассоциативном массиве, просто поиск по целому числу). См. ниже функцию
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=
: устанавливает разделитель для ввода в пустую строку, чтобы избежать разделения по пробелам.
Ключевые моменты при работе с байтовыми потоками
-
Обработка нулевых байтов: При работе с двоичными данными нужно понимать, что встреча нулевого байта (
0x00
) может привести к несоответствиям, поскольку многие инструменты Unix обрабатывают строки как null-терминированные. Чтение в Bash должно учитывать этот нюанс. -
Чтение и вывод в двоичном формате: Для вывода байтов в шестнадцатеричном формате, который часто более удобен для диагностики, можно использовать
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, для эффективной обработки данных. Важно учитывать различия в том, как строки и байты обрабатываются в разных языках, чтобы избежать неожиданных ошибок.