Подстановка параметров if-else

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

Я провожу серию экспериментов с bash и хочу сохранить журналы в директории, название которой основано на конфигурации эксперимента. Некоторые элементы конфигурации являются логическими (true/false). Пример конфигурации:

batch_size=16
fp16=false
bf16=true
checkpoint_activations=true

Я хотел бы сохранить журнал эксперимента с вышеуказанной конфигурацией в директории с следующим названием:

output_dir="experiment_bs${batch_size}_dt${fp16 if fp16=true else bf16}_${cp if checkpoint_activations=true else empty}"

Конечно, я мог бы объявить вспомогательные переменные:

data_type=""
"${fp16}" && data_type=fp16
"${bf16}" && data_type=bf16
"${cp}" && cp="_cp" || cp=""
output_dir="experiment_bs${batch_size}_dt${data_type}${cp}"

Но мне это кажется несколько громоздким, и я надеюсь, что замены параметров могут быть полезны здесь. "${bf16:+bf16}" не поможет в моем случае, потому что это всегда будет выводить “bf16”, независимо от его логического значения, пока оно определено.

Есть ли какие-либо замены параметров, которые можно применить к этому случаю использования? Или есть даже лучшее встроенное решение этой проблемы?

Замечание: есть причина, специфичная для приложения, по которой я не использую data_type напрямую в своей конфигурации.

В zsh вы можете определить функцию ? (и псевдоним, чтобы предотвратить ее обработку как шаблона glob), которая реализует форму тернарного оператора ? условие если-да если-нет, напоминающую оператор condition ? if-yes : if-no в C:

alias "?='?'"
'?'() if eval $1; then print -r -- $2; else print -r -- $3; fi

output_dir=experiment_bs${batch_size}_dt$(? $fp16 fp16 bf16)_$(? $cp cp)

С zsh 6.0+ (еще не выпущена на 2024-02-06) вы можете изменить это на:

alias "?='?'"
'?'() if eval $1; then REPLY=$2; else REPLY=$3; fi

output_dir=experiment_bs${batch_size}_dt${|? $fp16 fp16 bf16}_${|? $cp cp}

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

Обратите внимание, что этот тернарный оператор оценивает код в первом аргументе, чтобы решить, возвращать ли $2 или $3, поэтому ожидается, что $fp16/$cp будут содержать либо true, либо false. Измените на $(? '[[ $fp16 = true ]]' fp16 bp16), чтобы проверить, содержит ли $fp16 true или что-то еще.

Смотрите также это обсуждение на почтовом списке zsh для некоторых встроенных подходов к тернарному оператору. И этот вопрос и ответ о valsubs с подробностями об этом и альтернативах.

Если fp16 является переменной конфигурации, то я бы не делал "${fp16}" && data_type=fp16, так как это превращает переменную конфигурации в команду. Даже если мы не будем учитывать возможность того, что кто-то вставит что-то вроде reboot, даже опечатка вызовет странные сообщения об ошибках (например, “tru: команда не найдена”, или что-то подобное).

С другой стороны, возможно, это просто служит напоминанием проверить значения, которые получает ваш скрипт, например с помощью функции проверки, как:

checkbool() {
    case $1 in
        true|false) return 0;; 
        *) echo >&2 "'$1' является недопустимым логическим значением (должно быть 'true' или 'false')";
           exit 1;;
    esac
}
checkbool "$fp16"
checkbool "$bf16"
# ...

Также подумайте, имеет ли смысл fp16 и bf16 как независимые переменные?

В:

"${fp16}" && data_type=fp16
"${bf16}" && data_type=bf16

если оба fp16 и bf16 равны true, последний имеет приоритет. И если ни один не установлен, data_type остается пустым, что может оказаться допустимым или недопустимым.
Я не уверен в вашей точной ситуации, но мне интересно, было ли бы лучше просто иметь data_type как переменную конфигурации напрямую. Хорошо, в посте говорится, что есть причина не использовать data_type напрямую, но размышлять о том, что произойдет, если оба или ни одно из настроек не включено, может все еще иметь смысл.

Тем не менее, если вы хотите, чтобы замены параметров, такие как "${bf16:+bf16}", работали, вам нужно использовать пустое значение как ложное, а любую непустую строку как истинное. Тогда вы могли бы сделать, например, data_type="${enable_fp16:+fp16}", но даже это кажется трудным для использования, так как я не думаю, что есть хороший способ получить пустую строку, чтобы превратить ее в значение по умолчанию, не пропустив туда другое значение. Например, противоположная "${enable_fp16:-bf16}" превратила бы пустую строку в bf16, но она также вернула бы строку yes как есть.

И если вы собираетесь использовать пустые/непустые значения в скрипте, хотите ли вы раскрывать эту деталь пользователям в конфигурации? Или не будет ли лучше с точки зрения удобства использования записать условия, чтобы превратить значения конфигурации в те значения, которые на самом деле нужны скрипту, громоздко это или нет?

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

# конфигурация
batch_size=16
fp16=false
bf16=true
checkpoint_activations=true
## код
# это считает все, что не 'true', ложным
if   [[ $fp16  = true && $bf16 != true ]]; then
    data_type=fp16
elif [[ $fp16 != true && $bf16  = true ]]; then
    data_type=bf16
else
    echo >&2 "точно одно из fp16 и bf16 должно быть 'true'"
    exit 1
fi
cp=
if [[ $checkpoint_activations = true ]]; then
    cp=_cp
fi
# (возможно, значение $batch_size также должно быть проверено, что угодно

output_dir="experiment_bs${batch_size}_dt${data_type}${cp}"

Конечно, можно также проверить на каждом присваивании data_type, было ли оно уже установлено, вместо проверки значения каждой входной переменной на каждом условии. (Как сделано выше, добавление третьей переменной потребует изменений и в двух существующих условиях.)

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

choose() if [[ $1 = true ]]; then printf "%s\n" "$2"
         else printf "%s\n" "$3"
         fi
data_type=$(choose "$fp16" fp16 bf16)
# и т.д.

Конечно, выбор между многословным и явным кодом и компактным и лаконичным всегда остается за программистом.

Вы можете вставить любые команды bash, которые хотите, внутрь $(...), так что вы могли бы написать:

output_dir="experiment_bs${batch_size}_dt$([[ $fp16 = true ]] && echo $fp16 || echo $bf16)_$([[ $checkpoint_activation = true ]] && echo $cp || echo empty)"

Хотя для лучшей читаемости я мог бы написать вместо этого:

printf -v output_dir "experiment_bs%s_dt%s_%s" \
  "$batch_size" \
  "$([[ $fp16 = true ]] && echo "$fp16" || echo "$bf16")" \
  "$([[ $checkpoint_activation = true ]] && echo "$cp" || echo empty)"

Учитывая ваши образцы входных данных…

batch_size=16
fp16=false
bf16=true
checkpoint_activations=true

…оба варианта выше дают следующее значение:

experiment_bs16_dttrue_empty

В вашем примере выше, как уже было отмечено, установленное значение для bf16 всегда переопределяет fp16.

Когда это необходимо, поменяйте местами переменные и используйте :- (применяет значение по умолчанию ‘fp16’, если ‘bf16’ не задано или пусто), чтобы имитировать такое же поведение с помощью Замены параметров:

fp16=fp; bf16=bf
echo "${bf16:-$fp16}"

Для установки только одной из них используется соответствующее значение или пустое для ни одной.

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

Смотрите здесь

Поскольку в Bash вложенные Замены параметров не поддерживаются, поведение тернарного или ifelse можно имитировать с помощью двух связанных замен, как это:

isTrue=1; var1=hello; var2=world
"${isTrue:+$var1}" 2>/dev/null 
echo "${_:-$var2}"

Если условие isTrue, значение var1 ‘кешируется’ во временной переменной $_ на первом шаге (подавляя ошибку неназначения).

Наконец, $_ выводится, если непусто (если var1 установлено и непусто), в противном случае используется значение по умолчанию var2.

Полезно ли это пример, зависит от случая использования, конечно. Но это должно работать без каких-либо вызовов субшелла и быть довольно производительным.

Обратите внимание, что обе замены должны следовать друг за другом непосредственно, чтобы использовать $_.

Ответ или решение

Замена параметров с использованием конструкции If-Else в Bash

Ваша задача заключается в создании имени директории для хранения логов экспериментов, основываясь на конфигурации, которая включает булевы значения (true/false). В приведенном вами примере используется несколько переменных, таких как fp16, bf16 и другие. Давайте подробно разберем, как можно эффективно использовать условные операторы для замены параметров и формирования имени директории.

Пример конфигурации

batch_size=16
fp16=false
bf16=true
checkpoint_activations=true

Вы хотите сформировать путь к директории, в которой будут храниться логи, в следующем формате:

output_dir="experiment_bs${batch_size}_dt${fp16 if fp16=true else bf16}_${cp if checkpoint_activations=true else empty}"

Использование конструкций If-Else

Для создания более чистого и понятного кода мы можем использовать простые условия Bash. Это упростит код и сделает его более безопасным. Вот как это можно реализовать:

# Функция для проверки булевых значений
checkbool() {
    case $1 in
        true|false) return 0;; 
        *) echo >&2 "'$1' является недопустимым булевым значением (должно быть 'true' или 'false')"; exit 1;;
    esac
}

# Проверка значений
checkbool "$fp16"
checkbool "$bf16"
checkbool "$checkpoint_activations"

# Форматирование имени директории
if [[ "$fp16" == "true" && "$bf16" != "true" ]]; then
    data_type="fp16"
elif [[ "$fp16" != "true" && "$bf16" == "true" ]]; then
    data_type="bf16"
else
    echo >&2 "Только одно из значений fp16 и bf16 должно быть 'true'"
    exit 1
fi

if [[ "$checkpoint_activations" == "true" ]]; then
    cp="_cp"
else
    cp=""
fi

output_dir="experiment_bs${batch_size}_dt${data_type}${cp}"
echo "$output_dir"

Альтернативный способ: Упрощение с помощью функции

Можно создать функцию, чтобы избежать дублирования логики выбора между fp16 и bf16:

choose() {
    if [[ "$1" == "true" ]]; then
        printf "%s" "$2"
    else
        printf "%s" "$3"
    fi
}

data_type=$(choose "$fp16" "fp16" "bf16")
cp=$(choose "$checkpoint_activations" "_cp" "")
output_dir="experiment_bs${batch_size}_dt${data_type}${cp}"
echo "$output_dir"

Использование параметров в одной строке

Если вам нужна более компактная реализация, то вы можете использовать конструкции, которые возвращают значения через подстановки:

output_dir="experiment_bs${batch_size}_dt$([[ $fp16 == true ]] && echo "fp16" || echo "bf16")_$([[ $checkpoint_activations == true ]] && echo "_cp" || echo "")"
echo "$output_dir"

Заключение

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

Используйте предложенные методы и подходы для повышения эффективности вашей работы с Bash.

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

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