У меня есть следующий bash-скрипт, который выводит два случайных числа в одной строке.
#!/bin/bash
for i in 1 2; do
unset var
until [ "$var" -lt 10000 ] 2>/dev/null; do
var="$RANDOM"
done
printf "%s," "${var/%,/}"
done
Вывод:
5751,2129,
Я пытаюсь избавиться от запятой в конце, $var
с которой я пытался, "${var/%,/}"
чтобы вывод $var = 5751,2129 и его можно было использовать. Может ли кто-нибудь помочь с этим?
решение1
После назначения следующим образом: var="$RANDOM"
переменная var
содержит строку из расширения $RANDOM
. В Bash $RANDOM
расширяется до десятичного числа из диапазона от 0
до 32767
. Там нет ,
символа, поэтому нет смысла пытаться удалить ,
из расширения $var
позже.
Символы запятой, которые вы увидели в выводе, взяты из этой части кода:
printf "%s," "${var/%,/}"
# here ^
Эта команда вызывалась на каждой итерации цикла, поэтому каждая итерация добавляла к выходным данным один символ запятой.
То, что напечатано, не может быть распечатано. Есть нюансы:
- Вы можете передать вывод в фильтр, и фильтр может удалить некоторые его части. Фильтр может быть вне скрипта (например, вы вызываете
the_script | the_filter
); или вы можете передать вывод некоторыхчастьскрипта в фильтр внутри скрипта. В последнем случае вывод всего скрипта будет отфильтрован; все равно то, что было напечатано частью скрипта, не является ненапечатанным; я имею в виду, чтоделаетдобраться до фильтра. Фильтр его потом удалит. - Если вы печатаете на терминал, можно заставить его перезаписать некоторые части предыдущего вывода новыми данными. Есть символы и последовательности для перемещения курсора; все равно они все попадают на терминал. Вы можете визуально скрыть предыдущий вывод почти сразу, но если вы перенаправите вывод в файл (или на терминал, который не понимает используемые последовательности), то вы обнаружите, что все это там.
Правильный способ избавиться от нежелательной запятой — не печатать ее вообще; или отфильтровать ее внутри скрипта. Есть несколько способов сделать это, и я не собираюсь находить их все. Я рассмотрю некоторые из них. Я предполагаю, что вы хотите узнать возможные подходы также для циклов с более чем двумя итерациями; может быть, для циклов, где количество итераций заранее неизвестно; может быть, для циклов, не перечисленных числами; может быть, для циклов, которые могут никогда не закончиться (например, вместо while true
) for
.
Примечание: вы использовали printf "%s," "${var/%,/}"
, он не печатает завершающий символ новой строки. Я постараюсь воспроизвести это поведение, если это возможно.
Несколько возможных подходов:
Внутренность вашего цикла не зависит от
$i
. Вы можете избавиться от цикла и использовать две отдельные переменные:unset var1 until [ "$var1" -lt 10000 ] 2>/dev/null; do var1="$RANDOM" done unset var2 until [ "$var2" -lt 10000 ] 2>/dev/null; do var2="$RANDOM" done printf '%s,%s' "$var1" "$var2"
Примечания:
- Это неСУХОЙ.
- Он плохо масштабируется. (А что, если бы у вас был
for i in {1..100}
?) - Если количество итераций заранее неизвестно, это становится затруднительным.
Вы можете поместить ваш текущий код в конвейер и отфильтровать конечную запятую. Пример:
for i in 1 2; do unset var until [ "$var" -lt 10000 ] 2>/dev/null; do var="$RANDOM" done printf "%s," "$var" done | sed 's/,$//'
Примечания:
sed
(или любой другой фильтр, который вы используете) может или не может обрабатывать неполную строку (строку, не завершенную символом новой строки), которую создает цикл. В случае еслиsed
это зависит от реализации.- Если
sed
он это обрабатывает, он все равно может завершить свой вывод новой строкой. sed
(или любой другой фильтр, который вы используете) может не справиться со слишком длинной строкой. Приведенный выше конкретный код генерирует достаточно короткую строку, но в целом (представьте себе множество итераций) длина может быть проблемой.sed
, как инструмент, ориентированный на строки, должен прочитать всю строку, прежде чем обработать ее. В этом случае он должен прочитать весь свой ввод. Вы ничего не получите от него, пока не закончатся все итерации.- Цикл выполняется в подоболочке. В общем случае вам может понадобиться, чтобы он действовал в основной оболочке, по какой-либо причине.
Вы можете захватить вывод вашего текущего кода в переменную. В конце вы удаляете запятую, расширяя переменную:
capture="$(for i in 1 2; do unset var until [ "$var" -lt 10000 ] 2>/dev/null; do var="$RANDOM" done printf "%s," "$var" done)" printf "%s" "${capture%,}"
Примечания:
- Цикл выполняется в подоболочке. В общем случае вам может понадобиться, чтобы он действовал в основной оболочке, по какой-либо причине.
- Код молчит, пока не достигнет последнего
printf
(вне цикла). Вы ничего не получите, пока не закончатся все итерации. - В общем случае цикл может выводить любое количество байтов. Я думаю, что Bash может хранить значительное количество данных в переменной; затем, поскольку
printf
это встроенная функция, она можетвероятнообращатьсяprintf "%s" "${capture%,}"
без ударовограничение на длину командной строки. Я не проверял это тщательно, потому что, по моему мнению, хранение большого количества данных в переменной оболочки в любом случае не является лучшей практикой. Тем не менее, эта практика может быть оправдана, если вы знаете, что вывод ограничен по длине. (Для справки: приведенный выше код наверняка генерирует очень короткий вывод.) - Bash не может хранить символ(ы) NUL в переменной (большинство оболочек не могут; zsh может). Кроме того,
$()
удаляет все завершающие переводы строк. Это означает, что вы не можете использовать переменную для храненияпроизвольныйвывести и воспроизвести его позже точно. (Для справки: в приведенном выше коде фрагмент внутри$()
не генерирует NUL или завершающие символы новой строки.)
Вместо того, чтобы захватывать выходные данные, вы можете сделать так, чтобы каждая итерация добавлялась к некоторой переменной:
capture='' for i in 1 2; do unset var until [ "$var" -lt 10000 ] 2>/dev/null; do var="$RANDOM" done capture="$capture$var," done printf "%s" "${capture%,}"
Примечания:
- Код выполняется в основной оболочке (не в дочерней оболочке).
- Ограничения хранения данных в переменной (см. предыдущий метод) по-прежнему действуют.
- В Bash вы можете добавить к переменной с помощью
capture+="$var,"
. (Примечание: если для переменной установлен целочисленный атрибут, то=+
означает «добавить», а не «присоединить».)
Вы можете определить последнюю итерацию и использовать формат без
,
:# this example is more educative with more than two iterations for i in {1..5}; do unset var until [ "$var" -lt 10000 ] 2>/dev/null; do var="$RANDOM" done if [ "$i" -eq 5 ]; then printf "%s" "$var" else printf "%s," "$var" fi done
Примечания:
- Нет подоболочки.
- Определить последнюю итерацию сложнее, если вы заранее не знаете ее номер.
- Еще сложнее, если вы перебираете массив (например
for i in "${arr[@]}"
, ). - Каждая итерация печатает немедленно, вы получаете вывод последовательно. Это сработало бы, даже если бы цикл был бесконечным.
Вы можете обнаружить первую итерацию и использовать формат без
,
. Обратите внимание, что вы могли бы использовать,%s
вместо%s,
в исходном коде; тогда вы получите,5751,2129
вместо5751,2129,
. С этим изменением любой из вышеприведенных методов, который избегает или удаляет конечную запятую, может быть преобразован в метод, который избегает или удаляет начальную запятую. Самый последний метод становится:# this example is more educative with more than two iterations for i in {1..5}; do unset var until [ "$var" -lt 10000 ] 2>/dev/null; do var="$RANDOM" done if [ "$i" -eq 1 ]; then printf "%s" "$var" else printf ",%s" "$var" fi done
Примечания:
- Нет подоболочки.
- Определить первую итерацию легко, если всегда начинать с
1
(или с фиксированной уникальной строки в целом). - Но это сложнее, если вы итерируете по массиву, например
for i in "${arr[@]}"
. Вы не должны проверять,if [ "$i" = "${arr[1]}" ]
потому что может быть элемент, идентичный"${arr[1]}"
следующему в массиве. Простой способ справиться с этим — сохранить индекс для цикла (index=1
перед циклом, затем увеличивать на единицу в конце каждой итерации) и проверить его значение на1
; хотя я бы нашел такой код несколько громоздким. - Каждая итерация печатает немедленно, вы получаете вывод последовательно. Это сработало бы, даже если бы цикл был бесконечным.
Вы можете сделать
,
себя из переменной. Войдите в цикл с пустой переменной и установите ее,
в конце каждой итерации. Это фактически изменит значение только один раз в концепервыйитерация. Пример:# this example is more educative with more than two iterations separator='' for i in {1..5}; do unset var until [ "$var" -lt 10000 ] 2>/dev/null; do var="$RANDOM" done printf "%s%s" "$separator" "$var" separator=, done
Примечания:
- Нет подоболочки.
- Работает хорошо даже при итерации по массиву.
- Каждая итерация печатает немедленно, вы получаете вывод последовательно. Это сработало бы, даже если бы цикл был бесконечным.
Лично я нахожу этот метод весьма элегантным.
Главные примечания:
- Каждый фрагмент генерирует вывод без завершающего символа новой строки (за возможным исключением одного с
sed
или другого фильтра). Если вам нужно, чтобы весь вывод сформировал правильно завершенную строку текста, запуститеprintf '\n'
(или выполните singleecho
) после цикла. - Вы хотите
,
быть разделителем, а не терминатором. Это означает, что цикл с ровно нулевым числом итераций сгенерирует тот же пустой вывод, что и цикл с ровно одной итерацией, если$var
в итерации расширяется до пустой строки. В нашем случае$var
расширяется до непустой строки каждый раз, и мы знаем, что у нас больше нуля итераций; но в общем случае использование разделителя вместо терминатора может привести к неоднозначности.