Диалог Bash: замена цепочки команд в случае проблемы

Я хочу написать сценарий на основе dialog programbox (или других) и зафиксировать команду stderr и stdout, переданную dialog, в массив (если таковой имеется), а также статус завершения команды (в качестве последнего элемента массива). Для этого я использую подготовленные командные строки command substitution и fd3 redirection.

По сути, это работает хорошо, пока нет связанных команд с if — тогда я получаю сообщения об ошибках, например, в данном случае «команда не найдена». Вот (упрощенный) пример:

# Prepare command strings
declare cmd0='if [[ -e . ]]; then echo TRUE; fi'
cmd1=($("$cmd0") 2>&1)
cmd2=(dialog --colors --no-collapse --programbox "\Zb\Z1List local folders..." 20 160)

# Redirect stdout and stderr to dialog, while executing cmd1 > cmd2, capturing cmd1 exit status
exec 3>&1
tmp=($(${cmd1[@]} 2>&1 | "${cmd2[@]}" 1>&3; echo ${PIPESTATUS[0]}))
exec 3>&-

# Print results
printf '%s\n' "${tmp[@]}"

Кажется, что объявление команды требует объявления массива, но затем используются специальные символы, такие как ';' также добавит проблем. Это то, что я попробовал; прямое использование c вместо cmd1 приведет к печати всей строки как ошибки.

Что нужно изменить, чтобы command substituion для связанных команд (включая if) в cmd1 работал?

Вы уверены, что не хотите вместо этого определять функции? (Например, cmd0 () { if [[ -e . ]]; then echo TRUE; fi; }.

chepner 22.05.2024 21:50

Вам нужно $(eval "$cmd0")

Barmar 22.05.2024 21:50

Синтаксис оболочки не анализируется после расширения переменных.

Barmar 22.05.2024 21:51

Не храните команды в переменных. Переменные предназначены для хранения данных, функции — для хранения команд. Переменные расширяются на полпути в процессе синтаксического анализа команды, поэтому их значения анализируются лишь наполовину как синтаксис оболочки. Если вы используете eval, они, как правило, подвергаются полуторному анализу, что имеет раздражающую тенденцию нормально работать при тестировании, а затем странным образом терпеть неудачу через некоторое время. Так что просто не делайте этого. См. BashFAQ №50: «Я пытаюсь поместить команду в переменную, но в сложных случаях всегда терпит неудачу!» и множество предыдущих вопросов.

Gordon Davisson 22.05.2024 22:45

Отвечает ли это на ваш вопрос? Как сохранить команду в переменной в сценарии оболочки?

pjh 22.05.2024 23:47

Спасибо за ваш вклад. Проблема конкретно для dialog programbox заключается в том, что я хочу отслеживать любой прогресс в реальном времени, не дожидаясь завершения команды(ов). Только тогда общий вывод должен быть сохранен в переменной var, которая в данном случае будет только статусом завершения команды (а не из диалогового окна). Возможно, есть способ использовать tee, чтобы также захватить данные канала, но я пока не понял, как это сделать. Кроме того, даже если это работает со специальными функциями, это будет многовато только для создания команд dialog, а с вложенными командами будет довольно сложно.

fozzybear 23.05.2024 01:21

Хотя некоторые из приведенных выше предложений не полностью «отвечают на мой вопрос», такие как перенос команд для обработки в отдельные function, были приняты во внимание и применены в моем (окончательном) решении ниже. Другие предложения относительно правильного применения tee в примерах ниже помогли. В зависимости от варианта использования одно из частичных или альтернативных решений может работать лучше или быть достаточным. Однако окончательное решение, отвечающее всем требованиям, использует всего понемногу, плюс eval. Не стесняйтесь предлагать улучшения.

fozzybear 28.05.2024 23:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
87
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

С помощью подсказок в комментариях выше я мог бы найти решение, хотя и не совсем удовлетворительное, из-за необходимости определения functions для каждой сложной/вложенной команды для анализа/передачи dialog programbox.

dialog_programbox может принимать и анализировать простые команды через аргумент или, для сложных команд, вместо этого обрабатывать входные данные от stdin:

Может быть способ извлечь входные данные по конвейеру в захваченную переменную результата (здесь: exitStatus), например. г. за использование tee — если кто-то знает, как это сделать, пожалуйста, опубликуйте решение.

Для других вариантов dialog, которые принимают входные данные от переменной и не используют stdin, выходные данные dialog_programbox могут быть напечатаны в stdout как строка, разделенная символом новой строки, со статусом выхода/канала в качестве последнего или первого элемента, для легкого разделения из захваченный результат вар. Я, вероятно, расширю решение соответствующим образом позже.

# Function example for complex commands to pipe to dialog_programbox()
complexCmdFunc1() {
    # Requires explicit stderr redirection to stdout for each command
    # Could also split cmd execution and store exit status in var for reporting
    if ! git worktree remove -- "$1" 2>&1; then
        echo -e "\n\nREMOVAL BLOCKED:" "Worktree may not exist or is unlcean/locked (try force) - ABORTING."
        # Need to explicitly return exit status to report
        return 1
    fi
    echo -e "Successfully removed worktree:\n$1"
    return 0
}

dialog_programbox() {

    local -i height
    local -i width
    local -n cmd
    local cmdTmp
    local title
        
    for arg; do
        if [[ $arg == +([[:digit:]]) ]]; then
            if [[ $height -gt 0 ]]; then
                width=$arg
                continue
            fi
            height=$arg
        elif [[ $title ]]; then
            if [[ -R $arg ]]; then
                cmd=$arg
            # For direct CMD arg injection, has bug
            elif ! declare -p "$arg" 2>&1 1>/dev/null; then
                cmdTmp=$arg
                cmd=cmdTmp
            elif [[ $(declare -p "$arg" 2>/dev/null) == 'declare -a'* ]]; then
                cmd=$arg
            elif [[ -z $cmd ]]; then
                cmd=$arg
            fi
        else
            title = "$arg"
        fi  
    done
    
    [[ -z $height ]] && height=30
    [[ -z $width ]] && height=120

    # CMD ARG INJECTION
    if [[ $cmd ]]; then
        $( echo "${cmd[@]}" ) 2>&1 | dialog --title "$title" --colors --no-collapse --programbox $height $width 1>&3
        echo ${PIPESTATUS[0]}
        # stdin command substitution - would work but swallow pipe/exit status
        #dialog --title "$title" --colors --no-collapse --programbox $height $width 1>&3 < <($( echo ${cmd[@]} ) 2>&1)
    # PIPING STDIN
    else
        dialog --title "$title" --colors --no-collapse --programbox $height $width </dev/stdin 1>&3
        # Temp var would address empty stdin but waste real-time progress report
        #local tmp = "$(grep . </dev/stdin)"
        # if [[ $tmp ]] ; then
            # dialog --title "$title" --colors --no-collapse --programbox $height $width <<<"$tmp"
        # else
            # echo 'Nothing to process.' | dialog --title "$title" --colors --no-collapse --programbox $height $width
        # fi
    fi
}

# COMPLEX CMD output piping usage - requires explicit echo of PIPESTATUS
exitStatus1=$(complexCmdFunc1 'unreleased' | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 160; echo ${PIPESTATUS[0]})

# More efficient, since no additional subshell invocation is needed:
complexCmdFunc1 'unreleased' | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 160
exitStatus2=${PIPESTATUS[0]}

# Empty pipe, will render an empty dialog, of which addressing would require storing cmd result in temp var and losing real-time progress reporting
#exitStatus=$(echo | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 160; echo ${PIPESTATUS[0]})

# CMD var injection examples (for simple/non-nested commands)
#declare -a c=(ls foo bar)
declare c='ls foo bar'
exitStatus3=$(dialog_programbox '\Zb\Z1Deleting local worktree...' c 20 160)

# direct CMD injection - HAS BUG
#exitStatus=$(dialog_programbox '\Zb\Z1Deleting local worktree...' 'ls foo bar' 20 160)

echo -e "EXIT_STATUS1: $exitStatus1"
echo -e "EXIT_STATUS2: $exitStatus2"
echo -e "EXIT_STATUS3: $exitStatus3"

Другое решение, основанное на этом ответе Михала Горного (спасибо), пример того, как получить статус выхода И (отображаемый) вывод команды, определенной в функции и обработанной dialog programbox, с использованием coproc и глобальных переменных. :

# Result array for storing processed command's output
declare -ga cmdOutput
declare -g exitStatus
worktreeName='unreleased'

# Command to process and get exit status + results from
gitRemoveWtCmd() {
    _IFS = "$IFS"; IFS=$'\n'
    cmdOutput=( $(git worktree remove -- "$worktreeName" 2>&1) )
    exitStatus=$?
    IFS = "$_IFS"
    if [[ $exitStatus -ne 0 ]]; then
        printf '%s\n' "${cmdOutput[@]}"
        echo -e "\nCannot remove unclean or non-existent worktree" 
    else
        echo -e "\nSuccessfully removed worktree: [$worktreeName]"; 
    fi
}

# Co-process with dialog to send command results to, redirected output to fd3
coproc diaProgramboxCop { 
    dialog --colors --no-collapse --programbox '\Zb\Z1List local folders...' 20 160 1>&3
}

# Redirecting processed stderr to output and linking fd3 there
{ exec 3>&1
  gitRemoveWtCmd 2>&1
  exec 3>&-
} >&${diaProgramboxCop[1]}

exec {diaProgramboxCop[1]}>&-
wait ${diaProgramboxCop_PID}

printf '%s%s\n' "OUTPUT: " "${cmdOutput[@]}"
echo -e "EXIT_STATUS: $exitStatus"

Реальным недостатком здесь является то, что для cmdOutput и exitStatus требуются глобальные переменные, а захват вывода команды исключает отображение хода выполнения команды в реальном времени из-за его сохранения через подоболочку в var перед фактической отправкой в ​​dialog.

Таким образом, этот подход лучше использовать с widgets, например msgbox, которые используют предварительно заполненный var.

Согласно предложению здесь, используя fd3, перенаправленный из tee + cat в pipe, можно использовать для направления всего вывода команды в назначение var (stdout и stderr) и вместе с redirection вывода dialog непосредственно в /dev/tty, это другой способ захвата вывода обработанной команды и его отображения через dialog в режиме реального времени, при условии, что команды могут быть жестко закодированы и даже могут возвращать статус отсутствия прямого канала/выхода:

worktreeName='unreleased'

_IFS = "$IFS"; IFS=$'\n'
cmdOutput=( $({ gitRemoveWtCmd ${worktreeName@Q} 2>&1 | tee >(cat >&3) | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 120 >/dev/tty;} 3>&1 ; echo -e ${PIPESTATUS[0]}) )
printf '%s\n%s\n' "CMD_OUTPUT:" "${cmdOutput[@]:0:((${#cmdOutput[@]} - 1))}"
echo -e "EXIT_STATUS: ${cmdOutput[@]: -1}"
IFS = "$_IFS"

PIPESTATUS все еще можно распечатать из command substitution, но его необходимо отделить от захваченного вывода. Но еще лучше просто использовать ;exit ${PIPESTATUS[0]} из фигурных скобок подстановки того же процесса, обойтись без разделения массива и просто использовать код exit status из $? сразу в следующей строке.

Окончательное решение позволяет применять различные команды к dialog при использовании eval внутри function getDialogCmdResultAndExitStatus(), который принимает флаг для отключения любого вывода tty или stdout, а также var или name reference для команды для обработки и команду dialog для отображения , при этом по умолчанию все выходные данные команды также передаются в stdout (для записи в переменную) и всегда возвращаются внутренние pipe status обработанной команды как exit status, чтобы включить последующие решения в сценариях:

getDialogCmdResultAndExitStatus() {

    local quiet
    if [[ $1 =~ ^-[[:alnum:]] ]]; then
        if [[ ${1^^} != '-Q' ]]; then
            echo -e "Invalild argument: $1"
            exit 1
        fi
        quiet='TRUE'
        shift
    fi
    
    local -a com
    local -n cRef
    if ! declare -p "$1" &>/dev/null; then 
        com = "$1"
    else
        cRef=$1
        com = "${cRef[@]}"
    fi
    
    local -a diaCom
    local -n diaComRef
    if ! declare -p "$2" &>/dev/null; then 
        diaCom = "$2"
    else
        diaComRef=$2
        diaCom = "${diaComRef[@]}"
    fi
    
    if [[ $quiet ]]; then
        { eval "${com[@]@E} 2>&1" | eval "${diaCom[@]@E} >/dev/tty"; return ${PIPESTATUS[0]}; } 3>&1
    else
        { eval "${com[@]@E} 2>&1" | tee >(cat - >&3) | eval "${diaCom[@]@E} >/dev/tty"; return ${PIPESTATUS[0]}; } 3>&1
    fi
}

Используется следующим образом: использование обработанной команды exit status (внутренне pipe status) требует встроенного использования вместе с if или захвата $? для дальнейших решений сценария, сразу после строки захвата вывода:

# Prepare commands (utilizing functions from examples above)
declare command = "gitRemoveWtCmd ${worktreeName@Q}"
declare dialogCmd = "dialog_programbox '\Zb\Z1Deleting local worktree...' 20 120"

# Store and limit IFS to newlines
_IFS = "$IFS"; IFS=$'\n'
# Capture command's stdout and stderr + real-time displaying with dialog
cmdOutput=( $(getDialogCmdResultAndExitStatus command dialogCmd) )
# First line after output capturing: get pipe/exit status of processed command
echo -e "EXIT_STATUS: $?"
# Restore IFS to defaults
IFS = "$_IFS"

или

if ! getDialogCmdResultAndExitStatus command dialogCmd; then
    # do sth else...
fi

Внутреннюю команду eval можно еще улучшить, предложения приветствуются.

fozzybear 28.05.2024 23:09

Другие вопросы по теме