Bash: как удалить запятую в конце переменной, созданной циклом

Bash: как удалить запятую в конце переменной, созданной циклом

У меня есть следующий 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/%,/}", он не печатает завершающий символ новой строки. Я постараюсь воспроизвести это поведение, если это возможно.

Несколько возможных подходов:

  1. Внутренность вашего цикла не зависит от $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}?)
    • Если количество итераций заранее неизвестно, это становится затруднительным.
  2. Вы можете поместить ваш текущий код в конвейер и отфильтровать конечную запятую. Пример:

    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, как инструмент, ориентированный на строки, должен прочитать всю строку, прежде чем обработать ее. В этом случае он должен прочитать весь свой ввод. Вы ничего не получите от него, пока не закончатся все итерации.
    • Цикл выполняется в подоболочке. В общем случае вам может понадобиться, чтобы он действовал в основной оболочке, по какой-либо причине.
  3. Вы можете захватить вывод вашего текущего кода в переменную. В конце вы удаляете запятую, расширяя переменную:

    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 или завершающие символы новой строки.)
  4. Вместо того, чтобы захватывать выходные данные, вы можете сделать так, чтобы каждая итерация добавлялась к некоторой переменной:

    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,". (Примечание: если для переменной установлен целочисленный атрибут, то=+означает «добавить», а не «присоединить».)
  5. Вы можете определить последнюю итерацию и использовать формат без ,:

    # 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[@]}", ).
    • Каждая итерация печатает немедленно, вы получаете вывод последовательно. Это сработало бы, даже если бы цикл был бесконечным.
  6. Вы можете обнаружить первую итерацию и использовать формат без ,. Обратите внимание, что вы могли бы использовать ,%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; хотя я бы нашел такой код несколько громоздким.
    • Каждая итерация печатает немедленно, вы получаете вывод последовательно. Это сработало бы, даже если бы цикл был бесконечным.
  7. Вы можете сделать ,себя из переменной. Войдите в цикл с пустой переменной и установите ее ,в конце каждой итерации. Это фактически изменит значение только один раз в концепервыйитерация. Пример:

    # 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'(или выполните single echo) после цикла.
  • Вы хотите ,быть разделителем, а не терминатором. Это означает, что цикл с ровно нулевым числом итераций сгенерирует тот же пустой вывод, что и цикл с ровно одной итерацией, если $varв итерации расширяется до пустой строки. В нашем случае $varрасширяется до непустой строки каждый раз, и мы знаем, что у нас больше нуля итераций; но в общем случае использование разделителя вместо терминатора может привести к неоднозначности.

Связанный контент