Как подсчитать общее количество строк во всех файлах .txt?

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

Я пытаюсь понять, как получить общее количество строк из всех файлов .txt. Думаю, проблема на строке 6 -> let $((total = total + count )). Кто-нибудь знает, каков правильный вид этой строки?

#!/bin/bash
total=0
find /home -type f -name "*.txt" | while read -r FILE; do
          count=$(grep -c ^ < "$FILE")
           echo "$FILE has $count lines"
           let $((total = total + count ))
        done
        echo TOTAL LINES COUNTED:  $total

Спасибо

Ваша строка 6 лучше записывается как

total=$(( total + count ))

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

find . -name '*.txt' -type f -exec cat {} + | wc -l

Это находит все обычные файлы в текущей директории или ниже, у которых имена файлов заканчиваются на .txt. Все эти файлы объединяются в единый поток и передаются в wc -l, который выводит общее количество строк, что и требуется в заголовке и тексте вопроса.

Полный скрипт:

#!/bin/sh

nlines=$( find . -name '*.txt' -type f -exec cat {} + | wc -l )

printf 'Total number of lines: %d\n' "$nlines"

Чтобы получить также количество строк в отдельных файлах, рассмотрите

find . -name '*.txt' -type f -exec sh -c '
    wc -l "$@" |
    if [ "$#" -gt 1 ]; then
        sed "\$d"
    else
        cat
    fi' sh {} + |
awk '{ tot += $1 } END { printf "Total: %d\n", tot }; 1'

Это вызывает wc -l для пакетов файлов, выводя количество строк для каждого отдельного файла. Когда wc -l вызывается с более чем одним именем файла, он выводит строку в конце с общим количеством. Мы удаляем эту строку с помощью sed, если встраиваемый скрипт sh -c вызывается с более чем одним именем файла.

Длинный список количеств строк и имена файлов, затем передаются в awk, который просто складывает количества (и передает данные дальше) и представляет пользователю итоговое количество в конце.


В системах GNU, инструмент wc может считывать имена файлов из потока, разделенного символом nул. Вы можете использовать это с find и его действием -print0 в этих системах следующим образом:

find . -name '*.txt' -type f -print0 |
wc --files0-from=- -l

Здесь найденные имена файлов передаются как список, разделенный символом nул, через pipe в wc с использованием нестандартного -print0. Утилита wc используется с нестандартной опцией --files0-from для чтения передаваемого списка.

let $((total = total + count ))

Это работает, но немного избыточно, поскольку и let, и $(( .. )) начинаются с арифметического вычисления.

Любой из let "total = total + count", let "total += count", : $((total = total + count)) или total=$((total + count)) сделает это без дублирования. Последние два должны быть совместимы со стандартной оболочкой, let не является таковым.

total=0
find /home -type f -name "*.txt" | while read -r FILE; do
    total=...
done
echo TOTAL LINES COUNTED:  $total

Вы не сказали, какую проблему имеете в виду, но одна из проблем, с которой вы столкнетесь здесь, заключается в том, что в Bash части конвейера выполняются в подпроцессах по умолчанию, поэтому любые изменения, сделанные в total внутри цикла while, не видны после его завершения. Смотрите: Почему моя переменная локальна в одном цикле ‘while read’, а в другом аналогичном цикле – нет?

Вы можете использовать shopt -s lastpipe, чтобы последняя часть конвейера выполнялась в оболочке; или сгруппировать while и echo:

find ... | { while ...
    done; echo "$total"; }

Конечно, find ... | while read -r FILE; будет иметь проблемы с именами файлов, которые содержат переводы строк, или начинаются/заканчиваются на пробел. Вы можете исправить это с помощью

find ... -print0 | while IFS= read -r -d '' FILE; do ...

или, если вас не интересует разбивка по количеству строк в каждом файле и вы знаете, что ваши файлы являются полными текстовыми файлами, с отсутствующими лишь конечными переводами строк, вы можете просто объединить все файлы вместе и запустить wc -l на этом.

Если в ваших файлах может отсутствовать перевод строки в конце последней строки, и вы хотите считать эту финальную незавершенную строку, то вы не сможете сделать это, и вам нужно будет продолжать использовать grep -c ^ вместо wc -l. (Подсчет финальной частичной строки – практически единственная причина использовать grep -c ^ вместо wc -l.)

Смотрите: В чем смысл добавления новой строки в конец файла? и Почему текстовые файлы должны заканчиваться на перевод строки? на SO.

Также, если вас интересует только общее количество, все файлы, соответствующие шаблону, являются обычными файлами (поэтому тест -type f можно убрать), и у вас есть Bash и GNU grep, вы также можете сделать:

shopt -s globstar
shopt -s dotglob
grep -h -c ^ **/*.txt | awk '{ a += $0 } END { print a }'

**/*.txt это рекурсивный глоб, он требует явного включения для работы. dotglob делает так, что этот глоб также охватывает имена файлов, начинающиеся с точки. grep -h подавляет имена файлов в выводе, а скрипт awk подчитывает сумму. Поскольку имена файлов не печатаются, это должно работать, даже если некоторые из них проблематичны.

Или, как предложил @fra-san, на основе другого, теперь удаленного ответа:

grep -r -c -h --include="*.sh" ^ |awk '{ a+= $0 } END {print a }'

let total+=count будет работать, нет необходимости в $(( )) с этой формой арифметического вычисления.

Но вам было бы гораздо лучше сделать это с помощью wc -l.

find /home -type f -name '*.txt' -exec wc -l {} +

Если вы хотите пользовательский вывод, как в вашем скрипте выше, ИЛИ если будет больше имен файлов, чем влезет в ~2MB ограничение длины строки bash в Linux, вы можете использовать awk или perl для выполнения подсчета. Все лучше, чем цикл while-read в оболочке (см. Почему использование цикла while-read для обработки текста считается плохой практикой?). Например:

find /home -type f -name '*.txt' -exec perl -lne '
  $files{$ARGV}++;

  END {
    foreach (sort keys %files) {
      printf "%s has %s lines\n", $_, $files{$_};
      $total+=$files{$_}
    };
    printf "TOTAL LINES COUNTED: %s\n", $total
  }' {} +

Примечание: команда find ... -exec perl выше будет игнорировать пустые файлы, в то время как версия wc -l указала бы их с числом строк 0. Можно сделать так, чтобы perl делал то же самое (см. ниже).

С другой стороны, она будет вести подсчет строк и общую сумму для любого количества файлов, даже если они не все поместятся в одну командную строку оболочки – версия wc -l выдала бы две или больше строк total в этом случае – возможно, это не случится, но это не то, что вам нужно, если это произойдет.

Это должно работать, это использует wc -l и передает вывод в perl для изменения его в желаемый формат вывода:

$ find /home -type f -name '*.txt' -exec wc -l {} + |
    perl -lne 'next if m/^\s+\d+\s+total$/;
               s/\s+(\d+)\s+(.*)/$2 has $1 lines/;
               print;
               $total += $1;

               END { print "TOTAL LINES COUNTED:  $total"}'

Вместо того, чтобы читать каждую строку, что не очень оптимально, используйте wc.
Также исправьте синтаксис арифметической обработки: total=$((total+count)) будет полезным.

#!/bin/bash

total=0
path=/home

for f in $(find $path -type f -name "*.txt"); do
    count=$(wc -l < $f)
    echo "$FILE has $count lines"
    total=$((total + count))
done
echo TOTAL LINES COUNTED:  $total

Это не работает с именами файлов с пробелами или переводами строк.
Будьте осторожны.

Попробуйте это:

#!/bin/bash
export total=$(find . -name '*.txt' -exec wc -l "{}" ";" | awk 'BEGIN{sum=0} {sum+=$1} END{print sum}')
echo TOTAL LINES COUNTED ${total}

Если они все находятся в одной директории, тогда это работает:

cat -- *.txt | wc -l

(примечание: это не учитывает скрытые файлы, такие как .foo.txt, если вы не включили опцию dotglob или globdots в вашей оболочке)

С использованием Raku (ранее известного как Perl_6)

Адаптация отличного (первого) ответа на Perl5 от @cas здесь:

~$ find ~/find_dir -type f -name '*.txt' -exec raku -ne '
   BEGIN my %files; state $total;

         %files{$*ARGFILES}++;

   END   for (sort keys %files) {
         printf "%s has %s lines\n", $_, %files{$_};
         $total+=%files{$_};

   LAST  printf "TOTAL LINES COUNTED: %s\n", $total
         }' {} +

Значительные различия между оригинальным кодом на Perl5 и этим кодом на Raku включают неизменные символы — в частности, хэш %files никогда не меняет символы. В Raku файлы, прочитанные из командной строки, находятся в динамической переменной $*ARGFILES, хотя для более сложных скриптов можно использовать массив @*ARGS. Raku также имеет ряд управляющих команд, включая BEGIN, END и LAST, которые здесь использованы.

Начинайте заново в Raku, я бы, вероятно, написал что-то вроде следующего, что использует рутину dir(…) в Raku:

~$ raku -e '
   my  $total = 0;
   for dir("$*CWD/file_dir", test => /\.txt$/ ).sort -> $name {
       my  $lc = $name.lines(enc => "utf8-c8").elems; 
       say $name.absolute => $lc; 
       $total += $lc; 
   }; 
   say "TOTAL LINES COUNTED: $total";'

Поскольку рутина dir(…) в Raku может тестировать/фильтровать по строковому литералу (например, test => ".txt"), ИЛИ регулярному выражению (например, test => /\.txt$/), программисту не нужно полагаться на глобальную зону оболочки для фильтрации-включения только файлов .txt, представляющих интерес. Также, кодировка "utf8-c8" в Raku используется здесь, чтобы разгадать файлы с проблемными (UTF-8?) кодировками.

Входные данные:

books % ls
alice.txt       dracula.txt     huckfinn.txt        mobydick.txt        prideprej.txt       sherlock.txt        ulysses.txt
doriangrey.txt      greatexpectations.txt   janeeyre.txt        montecristo.txt     sensesensibility.txt    tomsawyer.txt

Выходные данные:

/Users/admin/carpalx-0.12/corpus/books/alice.txt => 3599
/Users/admin/carpalx-0.12/corpus/books/doriangrey.txt => 9195
/Users/admin/carpalx-0.12/corpus/books/dracula.txt => 16557
/Users/admin/carpalx-0.12/corpus/books/greatexpectations.txt => 21191
/Users/admin/carpalx-0.12/corpus/books/huckfinn.txt => 11718
/Users/admin/carpalx-0.12/corpus/books/janeeyre.txt => 22043
/Users/admin/carpalx-0.12/corpus/books/mobydick.txt => 17769
/Users/admin/carpalx-0.12/corpus/books/montecristo.txt => 62038
/Users/admin/carpalx-0.12/corpus/books/prideprej.txt => 14583
/Users/admin/carpalx-0.12/corpus/books/sensesensibility.txt => 15055
/Users/admin/carpalx-0.12/corpus/books/sherlock.txt => 13015
/Users/admin/carpalx-0.12/corpus/books/tomsawyer.txt => 8858
/Users/admin/carpalx-0.12/corpus/books/ulysses.txt => 32758
TOTAL LINES COUNTED: 248379

Вышеупомянутый код возвращает строковые пары IO::PATH => $lc, дающие количество строк как значение. Вызов метода absolute разрешает пути. Это достаточно просто, чтобы добавить эти пары в хэш (если необходимо) для дальнейших манипуляций. Как и в первом ответе, общее количество строк ($total) также выводится в последней строке.

https://docs.raku.org/language/control.html
https://docs.raku.org/routine/dir
https://docs.raku.org/language/io-guide#Stringifying_IO::Path
https://raku.org

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

Хотя это не лучший способ это сделать, вы можете использовать следующее:

shopt -s lastpipe
total=0
find pathhere -type f -name "*.txt" | while read FILE; do
     count=$(grep -c ^ < "$FILE")
     echo "$FILE has $count lines
     total=$((total + count))
done
echo TOTAL LINES COUNTED:  $total

или с wc:

shopt -s lastpipe
total=0
find pathhere -type f -name "*.txt" | while read FILE; do
     count=$(wc -l < "$FILE")
     echo "$FILE has $count lines"
     total=$((total + count))
done
echo TOTAL LINES COUNTED:  $total

Вы могли заметить shopt -s lastpipe, и это потому что цикл while выполняется в подпроцессе, и таким образом не передает новое значение переменной total в конце цикла…если не использовать эту опцию в начале.

Или если вы хотите что-то более быстрое и короткое:

find /path/to/directory/ -type f -name "*.txt" -exec wc -l {} \; | awk '{total += $1} END{print total}'

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

Чтобы посчитать общее количество строк во всех .txt файлах, вы можете воспользоваться несколькими методами, которые позволят осуществить эту задачу эффективно и корректно. Основная цель – получить общее количество строк из всех .txt файлов, находящихся в заданной директории. Я постараюсь объяснить, как это сделать на Bash и других инструментах командной строки.

Проблема и её решение

В представленной программе есть следующее замечание по строке:

let $((total = total + count ))

Эта строка имеет синтаксическую ошибку, так как объединяет два способа арифметического расширения. Правильнее будет написать:

total=$((total + count))

Эффективное решение

Вместо использования цикла while и команды grep, чтобы подсчитать количество строк в каждом файле, мы можем использовать команду wc (word count), специально предназначенную для подсчета строк, слов и символов. Вот как это можно сделать.

Пример с использованием find и wc

Если вы хотите просто посчитать общее количество строк из всех .txt файлов в текущей и вложенных директориях, используйте следующую команду:

find . -name '*.txt' -type f -exec cat {} + | wc -l

Объяснение:

  1. *`find . -name ‘.txt’ -type f`**: Находит все файлы с расширением .txt в текущей директории и поддиректориях.
  2. -exec cat {} +: Использует cat для вывода содержимого всех найденных файлов на экран.
  3. | wc -l: Подсчитывает количество строк во всех выводимых файлах.

Этот метод не только оптимален по скорости выполнения, но и прост в реализации.

Пример вывода количества строк в каждом файле

Если вам также нужно посчитать количество строк в каждом отдельном файле, можно использовать следующую команду:

find . -name '*.txt' -type f -exec wc -l {} + | awk '{print $2 " имеет " $1 " строк"} END {print "Общее количество строк: " total}' RS="\n"

Объяснение:

  1. wc -l: Возвращает количество строк в каждом файле.
  2. awk: Использован для форматирования вывода и подсчета общего количества строк.

Заключение

В современных разработках имеет значение не только достижение результата, но и эффективность. Использование wc вместо grep существенно упрощает скрипт и делает его более производительным. Ключевым элементом является правильный выбор инструментов для автоматизации задач, что особенно важно в профессиональной среде.

Надеюсь, это руководство поможет вам в решении задачи. Если у вас возникнут дополнительные вопросы, всегда обращайтесь за помощью.

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

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