Элегантный способ построить конвейер на основе возвращаемого значения, а не кода выхода?

8

Когда код состояния бесполезен, есть ли способ построить конвейер на основе вывода из stdout?

Я бы предпочел, чтобы ответ был не на сценарии использования, а на вопросе сценариев оболочки. Я пытаюсь найти наиболее конкретный пакет, доступный в репозитории, угадав имя на основе кодов страны и языка.

Возьмите, к примеру, это,

  • $PACKAGE1=hunspell-en-zz
  • $PACKAGE2=hunspell-en

Первое предположение более уместно, но оно может не существовать. В этом случае я хочу return hunspell-en( $PACKAGE2), потому что первая опция hunspell-en-zz( $PACKAGE1) не существует.

конвейеры apt-кеша

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

apt-cache возвращает ноль при нормальной работе, десятичное 100 при ошибке.

Это делает использование команды в конвейере более сложным. Обычно я ожидаю, что эквивалент 404 при поиске пакета приведет к ошибке (как это случилось бы с curlили wget). Я хочу выполнить поиск, чтобы увидеть, существует ли пакет, и если нет, использовать другой пакет, если он существует .

Это ничего не возвращает, так как первая команда возвращает успех (поэтому rhs в ||никогда не выполняется)

apt-cache search hunspell-en-zz || apt-cache search hunspell-en

apt-cache search с двумя аргументами

Это ничего не возвращает, поскольку apt-cacheANDs его аргументы,

apt-cache search hunspell-en-zz hunspell-en

Из документов apt-cache

Отдельные аргументы могут использоваться для указания нескольких шаблонов поиска, которые объединяются и объединяются.

Так как один из этих аргументов явно не существует, это ничего не возвращает.

Вопрос

Какова идиома оболочки для обработки соглашений, подобных тем, в apt-cacheкоторых код возврата бесполезен для задачи? А успех определяется только наличием выхода на STDOUT?

Похожий на

  • потерпеть неудачу, когда ничего не было найдено

    они оба вытекают из одной и той же проблемы. Выбранный ответ там упоминает, find -zчто, к сожалению, не применимо решение здесь и является конкретным вариантом использования. Там нет упоминания об идиоме или построении конвейера без использования нулевого завершения (опция не включена apt-cache)

Эван Кэрролл
источник
Вы уверены, что hunspell-enсуществует? В любом случае, вы можете использовать apt-cache policyи grep для ^$PACKAGENAME:.
AlexP
@AlexP это только примеры hunspell-en не существует, потому что они упаковывают с названиями стран, hunspell-arсуществуют и пакетов с названиями стран нет. Мне нужно найти наиболее точный пакет для данной страны и языка.
Эван Кэрролл
2
findкак apt-cacheв этом отношении - бесполезный код возврата, успех основан на выводе.
Муру
1
Да, я согласен, что они оба связаны с одной и той же проблемой. В выбранном ответе упоминается, -zчто, к сожалению, это не решение, поэтому проблема, связанная с конкретным случаем, не применима. И нет никакого упоминания об идиоме или построении конвейера без использования нулевого завершения (не apt-cache
Эван Кэрролл
1
@EvanCarroll нулевое окончание является необязательным. Я использовал его только потому, что это самый безопасный способ иметь дело с именами файлов, поэтому можно было бы ожидать, findчто он будет использоваться с -print0и, таким образом, grep с -z. Поскольку apt-cache не дает вывод с нулевым символом в конце, вам это не нужно -z.
Муру

Ответы:

5

Создайте функцию, которая принимает команду и возвращает true, если она имеет какой-либо вывод.

r() { local x=$("$@"); [ -n "$x" ] && echo "$x"; }

( ( r echo -n ) || echo 'nada' ) | cat      # Prints 'nada'
( ( r echo -n foo ) || echo 'nada' ) | cat  # Prints 'foo'

Так что для этого варианта использования это будет работать так,

r apt-cache search hunspell-en-zz || r apt-cache search hunspell-en
roaima
источник
Обратите внимание, r printf '\n\n\n'что вернет false. С другими оболочками zsh, r printf '\0\0\0'также будет возвращено ложное Так было бы r printf '\0a\0b\0c'с некоторыми снарядами.
Стефан Шазелас
3

Насколько я знаю, не существует стандартного способа справиться с теми случаями, когда успех команды определяется наличием выходных данных. Вы можете написать обходные пути, хотя.

Например, вы можете сохранить выходные данные команды в переменной, а затем проверить, является ли эта переменная пустой или нет:

output="$(command)"

if [[ -n "${output}" ]]; then
  # Code to execute if command succeded
else
  # Code to execute if command failed
fi

Я думаю, что это отвечает на вопрос в общих чертах, но если мы поговорим о apt-cache searchнекоторых решениях, мне в голову.

У меня есть скрипт, который делает управление пакетами проще. Вот некоторые из его функций:

search() {
  local 'package' 'packages'
  packages="$( apt-cache search '.*' | cut -d ' ' -f '1' | sort )"
  for package; do
    grep -F -i -e "${package}" <<< "${packages}"
  done
}


search_all() {
  local 'package'
  for package; do
    apt-cache search "${package}" | sort
  done
}


search_description() {
  local 'package' 'packages'
  packages="$( apt-cache search '.*' | sort )"
  for package; do
    grep -F -i -e "${package}" <<< "${packages}"
  done
}


search_names_only() {
  local 'package'
  for package; do
    apt-cache search --names-only "${package}" | sort
  done
}

Они позволяют выполнять несколько поисков в одной команде. Например:

$ search hunspell-en-zz hunspell-en
hunspell-en-au
hunspell-en-ca
hunspell-en-gb
hunspell-en-med
hunspell-en-us
hunspell-en-za

Каждая функция выполняет поиск в базе данных по-своему, поэтому результаты могут отличаться в зависимости от используемой вами функции:

$ search gnome | wc -l
538
$ search_all gnome | wc -l
1322
$ search_description gnome | wc -l
822
$ search_names_only gnome | wc -l
550
nxnev
источник
2

Я не назвал бы это изящным, но я думаю, что это могло бы сделать работу:

search_packages () {
    local packages=($@)
    local results=()
    for package in "${packages[@]}"; do
        results=($(apt-cache -n search "$package"))
        if [[ "${#results[@]}" -eq 0 ]]; then
            echo "$package not found."
        elif [[ "${#results[@]}" -eq 1 ]]; then
            do stuff with "$package"
        else
            echo "Warning! Found multiple packages for ${package}:"
            printf '\t-> %s\n' "${results[@]}"
        fi
    done
}

У меня нет машины Debian для тестирования, к сожалению. Я включил -nопцию «только для имен», apt-cacheчтобы попытаться ограничить результаты поиска, так как похоже, что вы в основном уверены в том, что ищете.

Может быть запущен как:

$ search_packages hunspell-en-zz hunspell-en
$ my_packages=('hunspell-en-zz' 'hunspell-en')
$ search_packages "${my_packages[@]}"
jesse_b
источник
1
Это именно то, о чем я думал, однако я искал что-то более изящное, поэтому давайте посмотрим, есть ли у кого-нибудь что-нибудь умнее (например, более абстрактное решение вне варианта использования), если нет, я отмечу это как выбрано.
Эван Кэрролл
1
В идеале, apt-cache просто возвращает что-то менее глупое.
Эван Кэрролл
1
@EvanCarroll, ты пробовал возиться с -qтихим вариантом? Страница man не очень многословна, но, возможно, она меняет возвращаемые значения?
jesse_b
1
по-прежнему возвращает 0. = (
Эван Кэрролл
2

Муру уточнил это в комментариях grepвернет статус 1, если нет ввода. Таким образом, вы можете добавить grep .в поток, и если нет ввода, соответствующего шаблону ., он изменит код состояния:

( ( echo -n | grep . ) || echo 'nada' ) | cat      # prints 'nada'
( ( echo -n foo | grep . ) || echo 'nada' ) | cat  # prints 'foo'

Для варианта использования, который выглядит следующим образом. В ниже нет, -pl-plпоэтому он отступает и возвращаетсяhunspell-pl

apt-cache search hunspell-pl-pl | grep . || apt-cache search hunspell-pl

Или,

apt-cache search hunspell-en-US | grep . || apt-cache search hunspell-en

Там -en-USтак и возвращается hunspell-en-us.

Смотрите также,

Эван Кэрролл
источник
grep .возвращает значение true, если входные данные содержат хотя бы одну (полностью ограниченную некоторыми реализациями) строку, которая содержит хотя бы один символ (хорошо сформированный для большинства реализаций), и в противном случае удалят пустые строки. grep '^'будет лучше работать при проверке наличия выходных данных, хотя в некоторых реализациях может все еще возвращать false, если входные данные представляют собой одну строку без разделителей (и может удалить эту строку или в других реализациях вернуть true, но добавить отсутствующий символ новой строки). Некоторые реализации grep также подавляют символ NUL.
Стефан Шазелас
2

Вы можете определить:

has_output() {
  LC_ALL=C awk '1;END{exit!NR}'
}

А потом:

if cmd | has_output; then
  echo cmd did produce some output
fi

Некоторые awkреализации могут подавить NUL-символы на входе.

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

Чтобы избежать этого и быть переносимым на системы, где awkзадыхается NUL, вы можете использовать perlвместо этого:

has_output() {
  perl -pe '}{exit!$.'
}

С помощью perlвы также можете определить вариант, который обрабатывает произвольные файлы более изящно:

has_output() {
  PERLIO=:unix perl -pe 'BEGIN{$/=\65536} END{exit!$.}'
}

Это ограничивает использование памяти (например, для файлов, в которых нет символов новой строки, таких как большие разреженные файлы).

Вы также можете создавать варианты, такие как:

has_at_least_one_non_empty_line() {
  LC_ALL=C awk '$0 != "" {n++};1; END{exit!n}'
}

или:

has_at_least_one_non_blank_line() {
  awk 'NF {n++};1; END{exit!n}'
}

(имейте в виду, что определение пробела варьируется в зависимости от awkреализации, в некоторых случаях оно ограничено пробелом и табуляцией, в некоторых также включены символы ASCII с вертикальным интервалом, такие как CR или FF, а в некоторых - пробелы локали)

В идеале в Linux вы должны использовать splice()системный вызов для максимизации производительности. Я не знаю команду, которая бы выставляла ее, но вы всегда можете использовать pythons ctypes:

has_output() {
  python -c 'if 1:
    from ctypes import *
    import sys
    l = CDLL("libc.so.6")
    ret = 1
    while l.splice(0,0,1,0,65536,0) > 0:
      ret = 0
    sys.exit(ret)'
}

(обратите внимание, что либо has_outputstdin, либо stdout (или оба) должны быть каналом для splice()работы).

Стефан Шазелас
источник
0

Я бы предложил использовать очень простые встроенные функции оболочки:

ck_command() { [ -n $("$@") ] ; }

Вот самый простой тестовый пример:

ck_command echo 1 ; echo $?

ck_command echo ; echo $?

Тогда вы можете легко использовать его с той ||конструкцией, к которой вы привыкли:

ck_command command_1 || ck_command command_2

Эта простая функция будет работать так, как вам хотелось бы с вашим apt_cacheповедением, каким бы ни было количество аргументов.

Дан
источник
За исключением этого теряет STDOUT в процессе, ck_command echo 'asdf' | catничего не выводит.
Эван Кэрролл
2
→ EvanCarroll: этого не было в вашем «Вопросе». Чтобы также добиться такого сохранения выходных данных, посмотрите на очень элегантный и простой ответ от @roaima: unix.stackexchange.com/a/413344/31707 .
дан