Вопрос или проблема
Я пытаюсь понять, как получить общее количество строк из всех файлов .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
Объяснение:
- *`find . -name ‘.txt’ -type f`**: Находит все файлы с расширением .txt в текущей директории и поддиректориях.
-exec cat {} +
: Используетcat
для вывода содержимого всех найденных файлов на экран.| wc -l
: Подсчитывает количество строк во всех выводимых файлах.
Этот метод не только оптимален по скорости выполнения, но и прост в реализации.
Пример вывода количества строк в каждом файле
Если вам также нужно посчитать количество строк в каждом отдельном файле, можно использовать следующую команду:
find . -name '*.txt' -type f -exec wc -l {} + | awk '{print $2 " имеет " $1 " строк"} END {print "Общее количество строк: " total}' RS="\n"
Объяснение:
wc -l
: Возвращает количество строк в каждом файле.awk
: Использован для форматирования вывода и подсчета общего количества строк.
Заключение
В современных разработках имеет значение не только достижение результата, но и эффективность. Использование wc
вместо grep
существенно упрощает скрипт и делает его более производительным. Ключевым элементом является правильный выбор инструментов для автоматизации задач, что особенно важно в профессиональной среде.
Надеюсь, это руководство поможет вам в решении задачи. Если у вас возникнут дополнительные вопросы, всегда обращайтесь за помощью.