Как сделать рекурсивный grep по сжатым архивам?

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

Я пытаюсь выяснить, какие модули 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 GNU tar для передачи содержимого каждого файла в 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, и т.д.) и выбирать инструменты, которые могут с этими форматами работать.

Теоретическая часть

Рекурсивный поиск внутри архивов — это задача, которая требует комбинации нескольких инструментов:

  1. Поиск текста. Для поиска строк в файлах традиционно используется утилита grep. Она позволяет искать текстовые строки, соответствующие заданному шаблону.
  2. Рекурсивный проход по структуре файлов. Это может быть реализовано с помощью команды find, которая позволяет пройтись по дереву каталогов и выбрать файлы, подходящие под заданные критерии.
  3. Доступ к архивам и извлечение содержимого. Для работы с архивами необходимы утилиты, которые могут разархивировать и извлекать данные: 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. Выбор правильного инструмента или комбинации утилит может существенно сократить время и улучшить точность поиска. Кроме того, стоит учитывать поддержку и возможности каждого инструмента по работе с различными типами архивов для построения оптимального решения в зависимости от конкретной задачи.

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

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