Вопрос или проблема
Я пытаюсь выяснить, какие модули use Test::Version
в cpan. Поэтому я использовал minicpan
, чтобы создать его зеркальную копию. Моя проблема в том, что мне нужно итеративно обойти скачанные архивы и использовать grep для поиска файлов в этих архивах. Может кто-нибудь подсказать, как мне это сделать? Желательно способом, который покажет мне, в каком файле архива и на какой строке это находится.
(примечание: это не все tarball, некоторые из них zip файлы)
Итак, давайте применим философию Unix. Каковы компоненты этой задачи?
- Поиск текста: вам нужен инструмент для поиска текста в файле, например,
grep
. - Рекурсивный: вам нужен инструмент для поиска файлов в дереве каталогов, например,
find
. - Архивы: вам нужен инструмент для их чтения.
Большинство программ Unix работают с файлами. Поэтому, чтобы легко работать с компонентами архива, вам нужно получить к ним доступ как к файлам, другими словами, вам нужно получить к ним доступ как к директориям.
Файловая система AVFS представляет собой представление файловой системы, где каждый файл архива /path/to/foo.zip
доступен как директория ~/.avfs/path/to/foo/zip#
. AVFS предоставляет доступ только для чтения к большинству распространенных форматов файлов архивов.
mountavfs
find ~/.avfs"$PWD" \( -name '*.zip' -o -name '*.tar.gz' -o -name '*.tgz' \) \
-exec sh -c '
find "$0#" -name "*.pm" -exec grep "$1" {\} +
' {} 'Test::Version' \;
fusermount -u ~/.avfs # не обязательно
Объяснения:
- Монтаж файловой системы AVFS.
- Поиск архивных файлов в
~/.avfs$PWD
, что является видом AVFS для текущей директории. - Для каждого архива выполняется указанный фрагмент оболочки (с
$0
= имя архива и$1
= искомый шаблон). $0#
это вид директории для архива$0
.{\}
, а не{}
, необходим в случае, если внешнийfind
заменяет{}
внутри аргументов-exec ;
(некоторые так делают, а некоторые нет).- Необязательно: в конце размонтируйте файловую систему AVFS.
Или в zsh ≥4.3:
mountavfs
grep 'Test::Version' ~/.avfs$PWD/**/*.(tgz|tar.gz|zip)(e\''
reply=($REPLY\#/**/*.pm(.N))
'\')
Объяснения:
~/.avfs$PWD/**/*.(tgz|tar.gz|zip)
совпадает с архивами в виде AVFS для текущей директории и её поддиректорий.PATTERN(e\''CODE'\')
применяет CODE к каждому совпадению PATTERN. Имя совпавшего файла находится в$REPLY
. Установка массиваreply
превращает совпадение в список имен.$REPLY\#
является видом директории для архива.$REPLY\#/**/*.pm
совпадает с файлами.pm
в архиве.- Квалификатор глоба
N
делает так, что шаблон разворачивается в пустой список, если нет совпадений.
Оказывается, я могу сделать это таким способом
find authors/ -type f -exec zgrep "Test::Version" '{}' +
Однако это дает такие результаты, как:
authors/id/J/JO/JONASBN/Module-Info-File-0.11.tar.gz:Binary file (standard input) matches
что не очень точно указывает, где находится в tarball. Надеюсь, кто-то сможет предложить более хороший ответ.
Используйте find для нахождения всех необходимых файлов, и zgrep для поиска в сжатых файлах:
find <folder> -type f -name "<search criteria[*gz,*bz...]>" -execdir zgrep -in "<grep expression>" '{}' ';'
Не тестировал на tarball
ugrep рекурсивно ищет сжатые файлы (gz/Z/bz2/lzma/xz/lz4/zstd) и архивы (cpio/tar/pax/zip) с опцией -z
. Опции -z --zmax=2
ищут сжатые файлы и архивы внутри сжатых файлов и архивов (поэтому zmax=2 уровня).
Спасибо за вызов, я придумал:
#!/bin/bash
#
# tarball для проверки в
find authors/ -type f | while read tarball; do
# получить список файлов в tarball (не каталоги, заканчивающиеся на /):
tar tzf $tarball | grep -v '/$' | while read file; do
# получить содержимое файла и искать строку
tar -Ozxf conform.tar.gz $file | grep -q 'Text::Version' && echo "Tar ($tarball) имеет совпадающий файл ($file)"
done
done
Может быть мой ответ будет полезен для кого-то:
#!/bin/bash
findpath=$(echo $1 | sed -r 's|(.*[^/]$)|\1/|')
# tarball для проверки в
find $findpath -type f | while read tarball; do
# получить список файлов в tarball (не каталоги, заканчивающиеся на /):
if [ -n "$(file --mime-type $tarball | grep -e "application/jar")" ]; then
jar tf $tarball | grep -v '/$' | while read file; do
# получить содержимое файла и искать строку
grepout=$(unzip -q -c $tarball $file | grep $3 -e "$2")
if [ -n "$grepout" ]; then
echo "*** $tarball имеет совпадающий файл ($file):"
echo $grepout
fi
done
elif tar -tf $tarball 2>/dev/null; then
tar -tf $tarball | grep -v '/$' | while read file; do
# получить содержимое файла и искать строку
grepout=$(unzip -q -c $tarball $file | grep $3 -e "$2")
if [ -n "$grepout" ]; then
echo "*** $tarball имеет совпадающий файл ($file):"
echo $grepout
fi
done
else
file=""
grepout=$(grep $3 -e "$2" $tarball)
if [ -n "$grepout" ]; then
echo "*** $tarball имеет совпадение:"
echo $grepout
fi
fi
done
После установки p7zip-*
вы можете сделать это:
ls | xargs -I {} 7z l {} | grep whatever | less
Вам не обязательно использовать ls
перед первым пайпом, любая команда, создающая список сжатых файлов, подойдет. Финальный less
будет показывать только PATH списка в сжатом архиве, но не его имя.
В zsh
и с bsdtar
+ GNU tar
+ GNU grep
, это может быть:
set -o extendedglob
for f (**/*.(#i)(zip|tar|(t|tar.)(xz|gz|bz2))(N.))
bsdtar cf - @$f | ARCHIVE=$f tar -xf - --to-command='
if [ "$TAR_FILETYPE" = f ]; then
grep -H --label="$ARCHIVE[$TAR_FILENAME]" Test::Version
fi
true'
Где
- глоб zsh ищет обычные (квалификатор glob
.
) не скрытые файлы, чье имя заканчивается на.zip
,.tar
,.tar.gz
,.tgz
… (без учета регистра). bsdtar
преобразует файл в формат архива ustar, который поддерживает GNU tar- мы используем
--to-command
GNUtar
для передачи содержимого каждого файла вgrep
grep
находит совпадения, отмечая их какfile.gz[file/in/archive]
.- мы завершаем скрипт
--to-command
сtrue
чтобы избежать предупрежденийtar
, когда эта команда возвращается с ненулевым статусом завершения:
Чтобы ограничить поиск до членов архива, чьё имя заканчивается на .pm
, вы можете изменить скрипт для --to-command
на:
case $TAR_FILETYPE$TAR_FILENAME in
(f*.pm) grep -H --label="$ARCHIVE[$TAR_FILENAME]" Test::Version
esac
true
Вот скрипт в dash
(также протестирован в bash
, zsh
, ksh
оболочках), который может искать в: .zip
, .7z
, .rar
, .tar.bz2
, .tar.xz
, .tar.gz
, .tgz
, .tar
, .bz2
, .xz
, .gz
архивах:
Чтобы запустить его: запустите без параметров, и он запросит необходимую информацию (считывает ввод с клавиатуры):
#!/bin/dash
PrintInTitle () {
printf "\033]0;%s\007" "$1"
}
PrintJustInTitle () {
PrintInTitle "$1">"$print_to_screen"
}
CleanUp () {
trap - INT
trap - TSTP
if [ -n "$TEMP_EXTR_PATH" ] && [ -n "$TEMP_EXTR_FOLDER" ]; then
rm -R -f "$TEMP_EXTR_PATH""/"'TEMP_EXTRACT_FOLDER'"/"*
fi
unset IFS
PrintJustInTitle ""
if [ "$1" = "1" ]; then
printf "Aborted\n">"$print_to_screen"
kill -s PIPE -- -$$ 2>/dev/null
fi
}
StoreArchiveFilePath () {
eval k=$(($k + 1))
eval archive_files_$k=\"\$archive_file\"
}
PrintMatch () {
printf '\n%s\n\n' "$search_path$current_archive_file/$inside_current_archive_file"|grep --color -F "$search_path$current_archive_file/$inside_current_archive_file"
for i in $(seq 1 $search_strings_0); do
eval current_search_string=\"\$search_strings_$i\"
printf '\n%s\n\n' "$current_search_string:"|grep --color -F "$current_search_string:"
cat "$inside_current_archive_file"|grep -i -n -F "$current_search_string" 2>/dev/null
done
}
ExtractFirstAndLastPathComponent () {
eval current_path="\"\$$1\""
first_path_component=""
last_path_component=""
if [ -n "$current_path" ]; then
#Remove trailing "https://unix.stackexchange.com/" characters:
while [ ! "${current_path%"/"}" = "$current_path" ]; do
current_path="${current_path%"/"}"
done
if [ -z "$current_path" ]; then
eval current_path=\"\$$1\"
fi
last_path_component="${current_path##*"/"}"
first_path_component="${current_path%"$last_path_component"}"
fi
eval $2="\"\$first_path_component\""
eval $3="\"\$last_path_component\""
}
GetCurrentContent () {
cd "$output_dir" && {
eval current_archive_file=\"\$$2\"
full_current_archive_file="$current_archive_file"
ExtractFirstAndLastPathComponent current_archive_file fpc_current_archive_file lpc_current_archive_file
current_archive_name_ext="${lpc_current_archive_file}"
case "$current_archive_file" in
*'.zip' )
unzip "$full_search_path/""$current_archive_file" -d "$output_dir"
;;
*'.7z' )
7z x "$full_search_path/""$current_archive_file" -o"$output_dir/"
;;
*'.rar' )
unrar x "$full_search_path/""$current_archive_file" "$output_dir/"
;;
*'.tar.bz2' )
tar -xvjf "$full_search_path/""$current_archive_file" -C "$output_dir"
;;
*'.tar.xz' )
tar -xvJf "$full_search_path/""$current_archive_file" -C "$output_dir"
;;
*'.tar.gz' | *'.tgz' )
tar -xvzf "$full_search_path/""$current_archive_file" -C "$output_dir"
;;
*'.tar' )
tar -xvf "$full_search_path/""$current_archive_file" -C "$output_dir"
;;
*'.bz2' | *'.xz' | *'.gz' )
cp "$full_search_path/""$current_archive_file" "$output_dir"
case "$current_archive_file" in
*'.bz2' ) bzip2 "$output_dir/""$current_archive_name_ext" -d "$output_dir"; ;;
*'.xz' ) xz "$output_dir/""$current_archive_name_ext" -d "$output_dir"; ;;
*'.gz' ) gzip -d "$output_dir/""$current_archive_name_ext"; ;;
esac
case "${current_archive_name_ext%"."*}" in
*'.tar' )
tar -xvf "$output_dir/${current_archive_name_ext%"."*}" -C "$output_dir"
rm "$TEMP_EXTR_PATH""/"'TEMP_EXTRACT_FOLDER'"/""${current_archive_name_ext%"."*}"
;;
esac
;;
esac >/dev/null 2>/dev/null
cd "$output_dir"
for inside_current_archive_file in $(for t in $(seq 1 $inside_archive_file_path_filters_0); do eval current_inside_archive_file_path_filter=\"\$inside_archive_file_path_filters_$t\"; eval find . -type f -path "$current_inside_archive_file_path_filter"|sort --numeric-sort; done;); do
gcc_found="true"
for i in $(seq 1 $search_strings_0); do
eval current_search_string=\"\$search_strings_$i\"
gcc_stored_content="$(cat "$inside_current_archive_file"|grep -i -n -F "$current_search_string" 2>/dev/null;)";
if [ -z "$gcc_stored_content" ]; then
gcc_found="false"
break
fi
done
if [ "$gcc_found" = "true" ]; then
if [ "$1" = "StoreArchiveFilePath" ]; then
StoreArchiveFilePath
break
elif [ "$1" = "PrintMatch" ]; then
PrintMatch
fi
fi
done
if [ -n "$TEMP_EXTR_PATH" ] && [ -n "$TEMP_EXTR_FOLDER" ]; then rm -R -f "$TEMP_EXTR_PATH""/"'TEMP_EXTRACT_FOLDER'"/"*; fi
cd "$full_search_path"
}
}
set +f #Enable globbing (POSIX compliant)
setopt no_nomatch 2>/dev/null #Enable globbing (zsh)
IFS='
'
print_to_screen='/dev/tty'
initial_dir="$PWD"
case "$(uname -s)" in
*"Linux"* )
TEMP_EXTR_PATH='/dev/shm' #TEMPORARY EXTRACT PATH
;;
*"Darwin"* | *"BSD"* | * )
TEMP_EXTR_PATH="$HOME" #TEMPORARY EXTRACT PATH
;;
esac
TEMP_EXTR_FOLDER='TEMP_EXTRACT_FOLDER' #TEMPORARY EXTRACT FOLDER
output_dir="$TEMP_EXTR_PATH""/"'TEMP_EXTRACT_FOLDER'
error="false"
{
cd "$TEMP_EXTR_PATH" && {
if [ ! -e "$TEMP_EXTR_FOLDER" ]; then
printf '%s\n' "The specified temporary directory: \"$TEMP_EXTR_FOLDER\" - does not exist in the specified location: \"$TEMP_EXTR_PATH\" - do you want to create it? [ Yes / No ] (default=Enter=No): ">"$print_to_screen"
read answer
if [ "$answer" = "Yes" ] || [ "$answer" = "yes" ] || [ "$answer" = "Y" ] || [ "$answer" = "y" ]; then
mkdir "$TEMP_EXTR_FOLDER" || error="true"
fi
fi
cd "$TEMP_EXTR_FOLDER" && output_dir="$PWD" || error="true"
} || error="true"
} 2>/dev/null
if [ "$error" = "true" ]; then
printf '%s\n' "Error: Could not access temporary folder \"$TEMP_EXTR_FOLDER\" in the extract location: \"$TEMP_EXTR_PATH\"!">&2
read temp
exit 1
fi
trap 'CleanUp 1' INT
trap 'CleanUp 1' TSTP
cd "$HOME"
printf '%s\n' "Search Path (blank=default=current folder=$PWD): "
read search_path
if [ -z "$search_path" ]; then
search_path="."
fi
printf '\n%s\n' "Inside archive file path filters: (what file path to lookup inside the archive) (concatenated internaly with logical OR) (default=Enter="*"): ">"$print_to_screen"
i=0
while [ "1" = "1" ]; do
printf '%s' ">> inside archive path filter: >> "
IFS= read -r current_inside_archive_file_path_filter
unset IFS
if [ -z "$current_inside_archive_file_path_filter" ]; then
break
fi
i=$(($i + 1))
eval inside_archive_file_path_filters_$i=\"\$current_inside_archive_file_path_filter\"
done
inside_archive_file_path_filters_0=$i
unset IFS #Reset IFS
if [ "$inside_archive_file_path_filters_0" = "0" ]; then
inside_archive_file_path_filters_1="'"'*'"'"
inside_archive_file_path_filters_0="1"
fi
printf '\n%s\n' "Search strings (concatenated internaly with logical AND):">"$print_to_screen";
i=0
while [ "1" = "1" ]; do
printf '%s' ">> add search string: >> "
IFS= read -r current_search_string
unset IFS
if [ -z "$current_search_string" ]; then break; fi;
i=$(($i + 1))
eval search_strings_$i=\"\$current_search_string\"
done
search_strings_0=$i
if [ "$search_strings_0" = "0" ]; then
search_strings_1=""
search_strings_0=1
fi
i=0
PrintJustInTitle "Loading list of archive files to analyze..."
IFS='
'
cd "$search_path" && {
full_search_path="$PWD"
{ find . \( -type f -path '*.zip' -o -path '*.bz2' -o -path '*.xz' -o -path '*.tar.*' -o -path '*.tgz' -o -path '*.tar' -o -path '*.gz' \) -exec printf "%s\n" "{}" \;|sort --numeric-sort;\
printf '%s\n' "..."; index=defined; while [ -n "$index" ]; do read index; printf '%s\n' $index; done; }|{
j=0; k=0
while read -r line; do
j=$(($j + 1))
if [ "$line" = "..." ]; then
break
else
PrintJustInTitle "Analyzing archive file $j..."
archive_file="$line"
fi
GetCurrentContent StoreArchiveFilePath archive_file
done
archive_files_0="$k"
PrintJustInTitle ""
if [ ! "$k" = "0" ]; then
index="defined"
count="$archive_files_0"
while [ -n "$index" ]; do
for i in $(seq 1 $count); do
eval current_archive_file=\"\$archive_files_$i\"
printf '\033[0;31m%s\033[0m\n' "$i = ""$current_archive_file"
done
printf '\n%s\n' "Print results (one at a time - blank = exit)?: [ 1 - $count ]: "
read index
if [ -z "$index" ]; then
break
fi
printf '%s\n' '---------------------------------------------------------------'
GetCurrentContent PrintMatch archive_files_$index
printf '%s\n\n' '---------------------------------------------------------------'
done
fi
}
}
PrintJustInTitle ""
CleanUp
Он потребляет много ресурсов, но вывод очень подробный.
.
Ответ или решение
Для решения задачи поиска внутри архивов, содержащих модули, использующие Test::Version
, в архивах CPAN, необходимо применить подход, который позволил бы нам рекурсивно обрабатывать сжатые файлы и архивы различных форматов. В данном контексте, важным аспектом является необходимость учитывать разнородность форматов архивов (например, tar
, zip
, tgz
, и т.д.) и выбирать инструменты, которые могут с этими форматами работать.
Теоретическая часть
Рекурсивный поиск внутри архивов — это задача, которая требует комбинации нескольких инструментов:
- Поиск текста. Для поиска строк в файлах традиционно используется утилита
grep
. Она позволяет искать текстовые строки, соответствующие заданному шаблону. - Рекурсивный проход по структуре файлов. Это может быть реализовано с помощью команды
find
, которая позволяет пройтись по дереву каталогов и выбрать файлы, подходящие под заданные критерии. - Доступ к архивам и извлечение содержимого. Для работы с архивами необходимы утилиты, которые могут разархивировать и извлекать данные:
tar
,unzip
,7zip
и т.д. Также существует возможность использования файловых систем типа AVFS, которые предоставляют поддержку чтения архивов как директорий.
Пример применения
Например, при использовании AVFS (опционально), вы можете монтировать ее следующим образом:
mountavfs
find ~/.avfs"$PWD" \( -name '*.zip' -o -name '*.tar.gz' -o -name '*.tgz' \) \
-exec sh -c 'find "$0#" -name "*.pm" -exec grep "$1" {} +' {} 'Test::Version' \;
fusermount -u ~/.avfs # Примерно размонтирование, если это необходимо
Здесь, ~/.avfs$PWD
предоставляет доступ к содержимому архивов как к директориям, что позволяет grep
искать внутри каждого файла в архиве.
Другой подход заключается в использовании таких инструментов, как zgrep
в связке с find
для поиска внутри сжатых файлов:
find <папка> -type f -name "<критерии поиска[*gz,*bz...]>" -execdir zgrep -in "<выражение для grep>" '{}' ';'
Однако этот метод может быть недостаточно конкретным, так как он не всегда точно указывает, в каком файле и на какой строке происходит совпадение.
Применение и сборка конечного решения
Для надежного, многоуровневого подхода можно воспользоваться скриптами, которые рекурсивно обходят файловую систему, извлекают файлы из архивов и выполняют поиск в них с гибкой поддержкой различных форматов архивов. Одно из таких решений включает использование целого ряда утилит командной строки, включая tar
, unzip
и 7zip
, для поддержки различных архивных форматов. Каждый инструмент в этом решении задействован в своей специализации и дополняет друг друга, что позволяет создать мощное комплексное решение для поиска.
Существуют готовые решения, такие как ugrep
с встроенной поддержкой обработки архивов и сжатых файлов, что значительно упрощает задачи, подобные поставленной:
ugrep -z --zmax=2 -r "Test::Version" .
Этот инструмент будет рекурсивно просматривать файлы и архивы, автоматически извлекая и анализируя их содержимое.
Заключение
Способность эффективно обрабатывать архивы и выполнять поиск в них требует хорошего понимания командной строки и возможностей инструментов, доступных в операционной системе на базе Unix. Выбор правильного инструмента или комбинации утилит может существенно сократить время и улучшить точность поиска. Кроме того, стоит учитывать поддержку и возможности каждого инструмента по работе с различными типами архивов для построения оптимального решения в зависимости от конкретной задачи.