Прочитать файл, несколько строк за раз (разделенные разделителем) - PullRequest
1 голос
/ 01 августа 2020

У меня есть большая коллекция записей BibTeX .bib, сохраненных в одном файле. Я хочу прочитать файл, сохранить данные каждой статьи (по существу разделенные @) в переменной, извлечь определенные поля c и, наконец, вывести поля (разделенные табуляцией) в очищенный файл.

Вход:

@article{Author1_2020,
    year = 2020,
    month = {feb},
    publisher = {Wiley},
    ...
}
@article{Author2_2010,
    year = 2010,
    month = {jul},
    publisher = {Journal},
    ...
}

Выход:

Wiley   2020    feb
Journal 2010    jul

Код:

while IFS='@' read -r entry; do
    p=$(grep "publisher =" <<< "$entry" | cut ...)
    y=$(grep "year =" <<< "$entry" | awk ...)
    m=$(grep "month =" <<< "$entry" | cut ...)
    echo "$p    $y  $m" >> cleaned_up.bib
done < global.bib
```sh

Is there a way to make the `while read` command in bash operate on delimited chunks of text at a time, instead of single lines? `sed`/`awk` solutions would be more than welcome.

Ответы [ 6 ]

4 голосов
/ 01 августа 2020

Другой подход с awk, который должен быть переносимым для всех awk разновидностей, может использовать '=' в качестве разделителя полей, например:

awk -F= '
    $1~/[ ]*year/       { year = substr($2,2,match($2,/,/)-2) }
    $1~/[ ]*month/      { month = substr($2,3,match($2,/,/)-4) }
    $1~/[ ]*publisher/  { pub = substr($2,3,match($2,/,/)-4) }
    FNR>1 && $1~/^@/    { print pub"\t"year"\t"month }
    END                 { print pub"\t"year"\t"month }
' list.bib

Где каждое из правил извлекает либо year, month или publisher и обрезает дополнительные символы с любого конца требуемой строки, используя substr() и match(). Правило END используется для печати окончательного набора собранных значений.

Пример использования / вывода

С данными вашего примера в list.bib, выполнение команды приведет к:

awk -F= '
    $1~/[ ]*year/       { year = substr($2,2,match($2,/,/)-2) }
    $1~/[ ]*month/      { month = substr($2,3,match($2,/,/)-4) }
    $1~/[ ]*publisher/  { pub = substr($2,3,match($2,/,/)-4) }
    FNR>1 && $1~/^@/    { print pub"\t"year"\t"month }
    END                 { print pub"\t"year"\t"month }
' list.bib
Wiley   2020    feb
Journal 2010    jul
3 голосов
/ 01 августа 2020

С GNU awk:

awk '{print $17, $6, $11}' RS='}\n' FS='( +|{|}|,)' OFS='\t' global.bib

Вывод:

Wiley   2020    feb
Journal 2010    jul

Я установил разделитель входных записей (RS) на }, за которым следует новая строка. По умолчанию - это новая строка.

Разделитель поля ввода (FS) я установил как минимум на один пробел ( +) или { или } или ,. OFS - разделитель выходных полей.

Другая запись с тем же выходом:

awk 'BEGIN{RS="}\n"; FS="( +|{|}|,)"; OFS="\t"} {print $17, $6, $11}' global.bib
2 голосов
/ 01 августа 2020

Всякий раз, когда входные данные содержат пары тег-значение, я считаю, что лучше сначала создать массив этого сопоставления (f[] ниже), а затем вы можете распечатать любые поля, которые вам нравятся, в любом порядке по их тегам (именам) :

$ cat tst.awk
BEGIN {
    OFS="\t"
    numTags = split(flds,tags)
}
/^}/ {
    for (tagNr=1; tagNr<=numTags; tagNr++) {
        tag = tags[tagNr]
        printf "%s%s", f[tag], (tagNr<numTags ? OFS : ORS)
    }
    delete f
    next
}
{
    gsub(/^[[:space:]]+|[[:space:],]+$/,"")
    tag = val = $0
    if ( sub(/^@/,"",tag) ) {
        sub(/\{.*/,"",tag)
        sub(/[^{]+\{|/,"",val)
    }
    else {
        sub(/[[:space:]]*=.*/,"",tag)
        sub(/[^=]+=[[:space:]]*/,"",val)
        gsub(/^\{|\}$/,"",val)
    }
    f[tag] = val
}

.

$ awk -v flds='publisher year month' -f tst.awk file
Wiley   2020    feb
Journal 2010    jul

.

$ awk -v flds='month year article publisher' -f tst.awk file
feb     2020    Author1_2020    Wiley
jul     2010    Author2_2010    Journal

Учитывая вышеупомянутый подход, вы можете тривиально добавить сравнения к коду внутри блока /^}/ { ... }, например

if ( (f["publisher"] == "Wiley") && (f["year"] == 2020) ) {
    do whatever you like
}

или вы можете настроить его так, чтобы просто преобразовать ваш ввод в CSV или JSON или любой другой формат, который вам нравится для вывода.

1 голос
/ 01 августа 2020

С GNU ed и column

Учитывая файл global.bib

@article{Author1_2020,
    year = 2020,
    month = {feb},
    publisher = {Wiley},
    mama = {foo},
    papa = {bar},
}
@article{Author2_2010,
    year = 2010,
    month = {jul},
    publisher = {Journal},
    mama = {foo},
    papa = {bar},
}
@article{Author3_2010,
    year = 2010,
    month = {aug},
    publisher = {Josh},
    mama = {foo},
    papa = {bar},
}
@article{Author4_2030,
    year = 2030,
    month = {dec},
    publisher = {Jetchisel},
    mama = {foo},
    blah = {qux},
    papa = {bar},
}

Скрипт ed, назовем его script.ed

g/./s/^@.*//\
s/^}.*//
v/^.*publisher =.*$\|^.*year =.*$\|^.*month =.*$\|^$/d
,s/^.*publisher = \|^.*year = \|^.*month = //
g/./s/}//\
s/{//\
s/,//
g/./s/$/ /
g/./;/^$/j
,s/\([^ ]*\) \([^ ]*\) \([^ ]*\)/\3 \1 \2/
g/^$/d
,p
Q

Теперь запустите сценарий ed для файла и направьте его в столбец с флагом -t.

ed -s global.bib < script.ed | column -t

На выходе

Wiley      2020  feb
Journal    2010  jul
Josh       2010  aug
Jetchisel  2030  dec

Краткое объяснение.

  • строки 1 и 2, поиск по всему файлу g означает глобальный, заменить все строки, начинающиеся с @ и } на ничего, сделайте это пустой строкой.

  • \ - это продолжение строки. поэтому строки 1 и 2 - это всего лишь одна строка, разделенная новой строкой.

  • строка 3, v означает противоположность тому, что совпадает внутри / /, в этом случае publisher, year и month плюс пустая / пустая строка, удалите ее, d означает удаление.

  • строка 4, ,s также является глобальным альтернатива для g. удалите все, что находится внутри / /, не удаляйте строки, которые его содержат, просто удалите это.

  • также подключены строки с 5 по 7, есть след \, удалить все который соответствует внутри / /, то есть {, } и ,

  • , строка 8 добавляет конечный пробел в файл.

  • строка 9, присоединение к непустой строке начинается с начала файла, g для глобального, пока не достигнет пустой строки.

  • строка 10 , сделайте обратную ссылку на все поля и распечатайте их в нужном порядке.

  • строка 11 удалить все пустые / пустые строки.

  • строка 12 ,p распечатать весь вывод в стандартный вывод.

  • строка 13, Q выйти без ошибки, даже если буфер был изменен, измените его на w, если он- необходимо редактировать файл.

  • Вы можете запускать скрипт ed построчно, просто включив все строки, разделенные \ и следующая строка после него, потому что это всего лишь один вызов ed.

С bash4+ grep и column

#!/usr/bin/env bash

limit=3

while mapfile -n "$limit" -t array; (( ${#array[*]} )); do
  array=("${array[@]//[\}\{,]}")
  array=("${array[@]#*= }")
  printf '%s %s %s\n' "${array[2]}" "${array[0]}" "${array[1]}"
done < <(
  grep -E '^[[:space:]]*(publisher|year|month) = ' global.bib
) | column -t
0 голосов
/ 02 августа 2020

Это может сработать для вас (GNU sed):

sed '/^@/{:a;N;/^}/M!ba;s/.*year = \(....\).*month = {\(...\)}.*publisher = {\([^}]*\)}.*/\3\t\1\t\2/}' file

Соберите строки между одной, начинающейся с @, и другой, начинающейся с }.

Используйте сопоставление с образцом для извлеките обязательные поля и разделите результат табуляцией.

NB Используйте флаг M в многострочном регулярном выражении, поскольку строки собираются в пространстве шаблона.

0 голосов
/ 01 августа 2020

Использование GNU awk:

awk 'match($0, /\s*(\S+)\s*=\s*\{?([^},]*)/, a) { r[a[1]]=a[2] }
     /^\}$/ { print r["publisher"], r["year"], r["month"] }
    ' OFS='\t' global.bib
...