Чтение за символом с bash read

8

Я пытался использовать bash для чтения файла символ за символом.

После долгих проб и ошибок я обнаружил, что это работает:

exec 4<file.txt 
declare -i n
while read -r ch <&4; 
     n=0
     while [ ! $n -eq ${#ch} ]
           do  echo -n "${ch:$n:1}"
               (( n++ ))
          done
     echo "" 
     done

Т.е. я могу читать это построчно, а затем перебирать каждую строчку символ за символом.

Перед этим я попытался: exec 4<file.txt && while read -r -n1 ch <&4; do; echo -n "$ch"; done но он пропустил бы все пробелы в файле .

Не могли бы вы объяснить, почему? Есть ли способ заставить работать вторую стратегию (то есть чтение char за char с чтением bash)?

PSkocik
источник
4
Устанавливать IFSничего не нужно, чтобы пробелы выживали при разделении слов.
manatwork
Попробовал это с IFS = '', но я думаю, это должен был быть просто IFS =. Спасибо!
PSkocik

Ответы:

12

Вам нужно удалить пробельные символы из $IFSпараметра, readчтобы прекратить пропускать начальные и конечные -n1символы (с помощью символа пробела, если он есть, и начальный, и конечный символы, поэтому пропускаются):

while IFS= read -rn1 a; do printf %s "$a"; done

Но даже тогда bash's readпропустит символы новой строки, с которыми вы можете обойти:

while IFS= read -rn1 a; do printf %s "${a:-$'\n'}"; done

Хотя вы можете использовать IFS= read -d '' -rn1вместо этого или даже лучше IFS= read -N1(добавлено в 4.1, скопировано из ksh93(добавлено в o)) команду, которая читает один символ.

Обратите внимание, что bash не readможет справиться с NUL-символами. И у ksh93 есть те же проблемы, что и у bash.

С зш:

while read -ku0 a; do print -rn -- "$a"; done

(Zsh может справиться с NUL-символами).

Обратите внимание, что они read -k/n/Nчитают количество символов , а не байтов . Поэтому для многобайтовых символов им, возможно, придется читать несколько байтов, пока не будет прочитан полный символ. Если входные данные содержат недопустимые символы, вы можете получить переменную, которая содержит последовательность байтов, которая не образует допустимых символов и которую оболочка может в итоге считать за несколько символов . Например, в локали UTF-8:

$ printf '\375\200\200\200\200ABC' | bash -c '
    IFS= read  -rN1 a; echo "${#a}"'
6

Это \375введет 6-байтовый символ UTF-8. Тем не менее, шестой ( A) выше недействителен для символа UTF-8. Вы по-прежнему получаете \375\200\200\200\200Ain $a, который bashсчитается как 6 символов, хотя первые 5 из них на самом деле не являются символами, только 5 байтов не являются частью какого-либо символа.

Стефан Шазелас
источник
Спасибо. Просто и красиво. На самом деле я пытался что-то сделать для этого (изменив переменную IFS), но у меня это не получилось, поэтому я закончил с этой смесью (ненужная игра с файловыми дескрипторами и т. Д.).
PSkocik
1
Интересно, что похоже, что использование read -rN1вместо этого решает проблему новой строки и, таким образом, устраняет необходимость предоставлять новую строку по умолчанию при печати $a.
krb686
Просто FTR я читаю 4118 строку 20 МБ файла. Использование read -n1(char by char) занимает 4 минуты 51 секунду и нагревает ноутбук до 90 градусов. Использование read -r(строка за строкой) занимает 1,3 секунды, а ноутбук работает на 54 градусах с двумя вентиляторами без звука.
WinEunuuchs2Unix
2

Это простой пример использования cut, а forцикл & wc:

bytes=$(wc -c < /etc/passwd)
file=$(</etc/passwd)

for ((i=0; i<bytes; i++)); do
    echo $file | cut -c $i
done

Поцелуй не так ли?

Жиль Квено
источник
Если это поцелуй, то что такое чистое bashрешение file="$(</etc/passwd)"; bytes="${#file}"; for ((i=0;i<bytes;i++)); do echo "${file:i:1}"; done?
manatwork
Спасибо обоим. Да, если мне придется прибегнуть к получению этих символов из строк, я мог бы также получить их из всего файла. Однако я считаю, что решение sch наиболее поцелуй.
PSkocik
@manatwork Это хорошее, простое решение. Несмотря на это, мне кажется, что приведенный выше ответ с использованием цикла чтения по какой-то причине несколько быстрее. Может быть, подстроки в bash довольно медленные?
krb686
@ krb686, на самом деле весь bash«он слишком большой и слишком медленный». в соответствии с разделом ошибок на странице справки. Но даже в этом случае все еще быстрее нарезать строку в памяти, чем читать файл снова и снова для каждого символа. По крайней мере, на моей машине: pastebin.com/zH5trQQs
manatwork