Bash - Почему $(файл sudo cat) не может найти существующий файл?

Вопрос

Почему $(sudo cat) не может найти существующий файл?

Это работает:

for host in $(cat /etc/ansible/hosts | cut -d ' ' -f 1 | grep -P '^master-' | sort | uniq)
do
    ssh ${host} /bin/bash << EOF
        sudo cat /etc/origin/master/ca-bundle.crt
EOF 
done

-----НАЧАЛО СЕРТИФИКАТА-----
XYZ...
-----КОНЕЦ СЕРТИФИКАТА-----

Это не работает, но не знаю, почему:

for host in $(cat /etc/ansible/hosts | cut -d ' ' -f 1 | grep -P '^master-' | sort | uniq)
do
    ssh ${host} /bin/bash << EOF
        RESULT="$(sudo cat /etc/origin/master/ca-bundle.crt; echo x)"
        echo "${RESULT%x}"
EOF
done

«cat: /etc/origin/master/ca-bundle.crt: нет такого файла или каталога»

Исправить

Основываясь на ответе, исправлено, как показано ниже, и сработало. Большое спасибо.

#!/bin/bash
set -u
for host in $(cat /etc/ansible/hosts | cut -d ' ' -f 1 | grep -P '^master-' | sort | uniq)
do
    ssh ${host} /bin/bash <<'EOF'
RESULT="$(sudo cat /etc/origin/master/ca-bundle.crt; echo x)"
echo "${RESULT%x}"
EOF
done

person mon    schedule 17.11.2017    source источник
comment
Я не думаю, что это дубликат - другой вопрос был о проблеме с процессом sudoed, который не наследовал FD от родителя; это связано с подстановкой команд в здесь-документах. Что еще более важно, решения в другом вопросе здесь неприменимы.   -  person Gordon Davisson    schedule 17.11.2017
comment
Прочитайте Часто задаваемые вопросы по Bash 001   -  person chepner    schedule 17.11.2017


Ответы (2)


Проблема возникает из-за того, что подстановки команд и переменных выполняются в здесь-документах до их передачи команде. Таким образом, $(sudo cat /etc/origin/master/ca-bundle.crt; echo x) и ${RESULT%x} оцениваются на локальном компьютере (где /etc/origin/master/ca-bundle.crt предположительно не существует, а RESULT не определен). В результате вот что отправляется на удаленный компьютер:

    RESULT="x"
    echo ""

Это совсем не то, что вы хотели.

Чтобы избежать этого, вы можете либо экранировать $ символов в здесь-документе, либо заключить в кавычки разделитель здесь-документа (<<'EOF'). Кстати, символы отступа в здесь-документе будут передаваться через соединение ssh, что может иметь неприятные последствия (например, вызов функций автозаполнения удаленной оболочки). Я бы либо выделил здесь документ, либо поставил перед разделителем префикс «-» (<<-'EOF') и использовал табуляцию (а не пробелы) для отступа.

РЕДАКТИРОВАТЬ: Подумав об ответе @tripleee, я не уверен, что согласен со всеми его предложенными переписываниями, но он, безусловно, прав в том, что части цикла более сложны и/или хрупки, чем они должны быть. Вот мои предложения по изменениям:

  • Если на удаленном компьютере не делается больше, чем показано в вопросе, часть ssh может (и должна) быть радикально упрощена. Нет необходимости помещать содержимое файла сертификата в переменную, а затем отображать переменную, просто выведите ее напрямую. И обратите внимание, что вся проблема возникла из-за сложностей с вводом и выводом переменной; если бы удаленная часть была просто sudo cat /etc/origin/master/ca-bundle.crt, все бы просто работало. Кроме того, нет необходимости явно запускать удаленную оболочку bash и использовать перенаправление, чтобы дать ей команду для запуска, просто используйте ssh ${host} sudo cat /etc/origin/master/ca-bundle.crt.

    Хорошо, есть одна вещь, которую сделали переменные. Он добавил пустую строку после содержимого файла сертификата. Это происходит из-за того, что трюк с "x" тщательно сохраняет последнюю новую строку в содержимом файла (обычно $( ) удаляет конечные символы новой строки), а затем echo добавляет еще одну новую строку, в результате чего получается пустая строка. Но если это было сделано намеренно, то гораздо проще (и понятнее) просто добавить еще одну команду echo без аргументов.

Конвейер для перечисления имен хостов намного длиннее, чем нужно, но здесь есть компромисс между сложностью и удобочитаемостью/ясностью. Версия tripee намного короче, но из нее сложнее понять, что она делает (если вы не владеете свободно awk), а это означает, что существует больше риска ошибок, ее сложнее поддерживать и т. д. Но есть еще некоторые упрощения, которые я бы хотел рекомендовать:

  • Не используйте cat для передачи одного файла в конвейер. Либо используйте перенаправление <filename, либо (если команда его поддерживает) просто дайте команде имя файла и дайте ей прочитать из него:

    cut -d ' ' -f 1 /etc/ansible/hosts | ...
    cut -d ' ' -f 1 </etc/ansible/hosts | ...
    

    Это может показаться менее естественным, чем использование cat |, но это лучший (и самый стандартный) способ сделать что-то. Откровенно говоря, если это кажется неестественным или запутанным, это просто означает, что вы сделали это недостаточно. Так что делайте это больше.

  • В вашем пайплайне каждая команда делает только одно: cut получает первое поле, grep выбирает те, которые начинаются с "master-", sort упорядочивает их, а uniq удаляет дубликаты. Но sort вполне способен сам удалять дубликаты, поэтому я бы использовал sort -u вместо sort | uniq. И awk может делать почти все сам по себе (но становится трудно читать/понимать, если вы заставите его делать слишком много). Лично я бы использовал:

    awk '$1 ~ /^master-/ {print $1}' /etc/ansible/hosts | sort -u
    

    (Этот сценарий awk можно прочитать как «если первое поле начинается с 'master-', напечатайте его.)

Тогда возникает вопрос, как перебрать список хостов. for var in $(somecommand) широко считается антипаттерном из-за множества вещей, которые могут пойти не так. Он разбивает вывод команды на «слова» (которые могут соответствовать или не соответствовать строкам), а затем пытается преобразовать все найденные подстановочные знаки в списки соответствующих файлов (что может иметь действительно странные эффекты ), а затем перебирает результат этого.

В данном конкретном случае, я думаю, вы в безопасности (если только вы не переопределили $IFS). В ваших записях не должно быть разделителей слов (пробелов, табуляции и новой строки), потому что вы уже выбираете только первое поле. В записях не должно быть подстановочных знаков, потому что «*», «?» и «[» обычно не допускаются в именах хостов. Я предполагаю, что может быть необработанный IPv6-адрес в формате "[hexdigits:hexdigits::hexdigits]", который является подстановочным знаком оболочки и будет вызывать проблемы, если есть совпадения файлы, но они не будут выбраны по шаблону ^master-.

В качестве альтернативы можно использовать передачу списка либо команде xargs (как в ответе @tripleee), либо циклу while read. У них обоих есть другая потенциальная проблема, заключающаяся в том, что ssh может украсть имена хостов из входного потока, если вы не будете осторожны. ssh -n предотвратит это.

В этом случае я думаю, что просто использовал бы цикл for. Итак, со всем этим, вот мое предложенное переписывание:

#!/bin/bash
set -u
for host in $(awk '$1 !~ /^master-/ {print $1}' /etc/ansible/hosts | sort -u); do
    ssh "${host}" sudo cat /etc/origin/master/ca-bundle.crt
    echo
done
person Gordon Davisson    schedule 17.11.2017

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

#!/bin/bash
awk '$1 !~ /^master-/ && !a[$1] { print $1; ++a[$1] }' /etc/ansible/hosts |
xargs -r -n 1 ssh {} sudo cat /etc/origin/master/ca-bundle.crt

Это избавляет от добавления и последующего удаления завершающего x -- я подозреваю, что вы в конечном итоге оказались с этим в первую очередь, потому что вы захватывали, а затем echo выводили вместо того, чтобы просто позволить cat делать то, что он делает -- печатать на стандартный вывод.

person tripleee    schedule 17.11.2017