Вопрос или проблема
Я провожу серию экспериментов с 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
вложенные Замены параметров
не поддерживаются, поведение тернарного
или if
–else
можно имитировать с помощью двух связанных замен, как это:
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.