Как выполнить итерацию по диапазону чисел, определяемых переменными в Bash?

Как выполнить итерацию по диапазону чисел в Bash, когда диапазон задается переменной?

Я знаю, что могу это сделать (называется "выражение последовательности" в Bash документация):

 for i in {1..5}; do echo $i; done

что дает:

1
2
3
4
5

тем не менее, как я могу заменить любую из конечных точек диапазона переменной? Это не работает:

END=5
for i in {1..$END}; do echo $i; done

что принты:

{1..5}

17 ответов


for i in $(seq 1 $END); do echo $i; done

edit: я предпочитаю seq над другими методами, потому что я это помню ;)


на seq метод является самым простым, но Bash имеет встроенную арифметическую оценку.

END=5
for ((i=1;i<=END;i++)); do
    echo $i
done
# ==> outputs 1 2 3 4 5 on separate lines

на for ((expr1;expr2;expr3)); строительство работает так же, как for (expr1;expr2;expr3) на C и подобных языках, и как другие ((expr)) случаи, Bash рассматривает их как арифметику.


обсуждение

используя seq прекрасно, как и предположил Цзяаро. Pax Diablo предложил цикл Bash, чтобы избежать вызова подпроцесса, с дополнительным преимуществом быть более дружественным к памяти, если $END слишком велик. Zathrus заметил типичную ошибку в реализации цикла, а также намекнул, что с i - это текстовая переменная, непрерывные преобразования чисел туда и обратно выполняются с соответствующим замедлением.

целочисленной арифметики

это улучшенная версия цикла Bash:

typeset -i i END
let END=5 i=1
while ((i<=END)); do
    echo $i
    …
    let i++
done

если единственное, что мы хотим-это echo, тогда мы могли бы написать echo $((i++)).

ephemient научил меня кое-чему: Баш позволяет for ((expr;expr;expr)) конструктов. Поскольку я никогда не читал всю man-страницу для Bash (как я сделал с оболочкой Korn (ksh) man page, и это было давно), я пропустил это.

и

typeset -i i END # Let's be explicit
for ((i=1;i<=END;++i)); do echo $i; done

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

первый вопрос

eschercycle отметил, что {a..b} нотация Bash работает только с литералами; true, соответственно руководству Bash. Преодолеть это препятствие можно одним (внутренним)fork() без exec() (как в случае с вызовом seq, который будучи другое изображение требует fork + exec):

for i in $(eval echo "{1..$END}"); do

и eval и echo являются bash builtins, но a fork() требуется для замены команды ($(…) построить).


вот почему исходное выражение не сработало.

С man bash:

расширение расчалки выполнено перед любые другие расширения и любые характеры специальные к другим расширения сохранены в результат. Она строго текстуальна. Удар не применяет синтаксические толкование контекста расширение или текст между фигурные скобки.

и фигурные скобки is что-то сделано раньше как чисто текстовая макрооперация, до расширения параметров.

оболочки - высоко оптимизированные гибриды между макропроцессорами и более формальными языками программирования. Для оптимизации типичных случаев использования язык становится более сложным и принимаются некоторые ограничения.

рекомендация

Я бы предложил придерживаться Posix1 функции. Это означает использование for i in <list>; do, если список уже известен, в противном случае, используйте while или seq, например:

#!/bin/sh

limit=4

i=1; while [ $i -le $limit ]; do
  echo $i
  i=$(($i + 1))
done
# Or -----------------------
for i in $(seq 1 $limit); do
  echo $i
done


1. Bash-отличная оболочка, и я использую ее в интерактивном режиме, но я не помещаю bash-isms в свои скрипты. Скриптам может потребоваться более быстрая оболочка, более безопасная, более встроенная. Им может потребоваться запустить все, что установлено как /bin / sh, а затем есть все обычные аргументы pro-standards. Помни!--7-->shellshock, ака bashdoor?

путь POSIX

если вы заботитесь о переносимости, используйте пример из стандарта POSIX:

i=2
end=5
while [ $i -le $end ]; do
    echo $i
    i=$(($i+1))
done

выход:

2
3
4
5

вещи не POSIX:


еще один уровень косвенности:

for i in $(eval echo {1..$END}); do
    ∶

можно использовать

for i in $(seq $END); do echo $i; done

Если вы находитесь на BSD / OS X, вы можете использовать jot вместо seq:

for i in $(jot $END); do echo $i; done

это отлично работает в bash:

END=5
i=1 ; while [[ $i -le $END ]] ; do
    echo $i
    ((i = i + 1))
done

Если вам нужен префикс, чем вам может понравиться это

 for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done

что даст

07
08
09
10
11
12

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

$ ksh -c 'i=5; for x in {1..$i}; do echo "$x"; done'
1
2
3
4
5
$ ksh -c 'echo $KSH_VERSION'
Version JM 93u+ 2012-02-29

$ bash -c 'i=5; for x in {1..$i}; do echo "$x"; done'
{1..5}

это другой способ:

end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done

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

Если вы заключаете свой цикл for В двойные кавычки, начальная и конечная переменные будут разыменованы при Эхо-строке, и вы можете отправить строку обратно в BASH для выполнения. $i должен быть экранирован с \ ' S, поэтому он не оценивается перед отправкой в подсеть.

RANGE_START=a
RANGE_END=z
echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \${i}; done" | bash

этот вывод также может быть назначен a переменная:

VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \${i}; done" | bash`

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


заменить {} С (( )):

tmpstart=0;
tmpend=4;

for (( i=$tmpstart; i<=$tmpend; i++ )) ; do 
echo $i ;
done

выходы:

0
1
2
3
4

Если вы выполняете команды оболочки, и у вас (как у меня) есть фетиш для конвейеризации, это хорошо:

seq 1 $END | xargs -I {} echo {}


если вы хотите оставаться как можно ближе к синтаксису выражения скобки, попробуйте range функция от bash-tricks'range.bash.

например, все следующее будет делать то же самое, что и echo {1..10}:

source range.bash
one=1
ten=10

range {$one..$ten}
range $one $ten
range {1..$ten}
range {1..10}

он пытается поддерживать собственный синтаксис bash с как можно меньшим количеством "gotchas": поддерживаются не только переменные, но и часто нежелательное поведение недопустимых диапазонов, предоставляемых в виде строк (например,for i in {1..a}; do echo $i; done) является предотвращено также.

другие ответы будут работать в большинстве случаев, но все они имеют хотя бы один из следующих недостатков:

  • многие из них используют подоболочек, который может производительность вред и не возможно на некоторых системах.
  • многие из них полагаются на внешние программы. Даже seq это двоичный файл, который должен быть установлен для использования, должен быть загружен bash и должен содержать программу, которую вы ожидайте, что это сработает в данном случае. Вездесущий или нет, это намного больше, чем полагаться на сам язык Bash.
  • решения, которые используют только собственные функции Bash, такие как @ephemient, не будут работать в алфавитных диапазонах, таких как {a..z}; фигурные скобки будет. Вопрос был о диапазонах цифры, хотя, так что это придирка.
  • большинство из них визуально не похожи на {1..10} brace-расширенный синтаксис диапазона, поэтому программы, которые используют оба могут быть немного труднее читать.
  • @bobbogo's answer использует некоторые из знакомого синтаксиса, но делает что-то неожиданное, если $END переменная не является допустимым диапазоном "bookend" для другой стороны диапазона. Если END=a, например, ошибка не произойдет и дословное значение {1..a} будет вторит. Это также поведение Bash по умолчанию-это просто часто неожиданно.

отказ от ответственности: я являюсь автором связанный код.


это работает в Bash и Korn, также может перейти от более высоких чисел к более низким. Наверное, не самый быстрый или красивый, но работает достаточно хорошо. Обрабатывает негативы тоже.

function num_range {
   # Return a range of whole numbers from beginning value to ending value.
   # >>> num_range start end
   # start: Whole number to start with.
   # end: Whole number to end with.
   typeset s e v
   s=
   e=
   if (( ${e} >= ${s} )); then
      v=${s}
      while (( ${v} <= ${e} )); do
         echo ${v}
         ((v=v+1))
      done
   elif (( ${e} < ${s} )); then
      v=${s}
      while (( ${v} >= ${e} )); do
         echo ${v}
         ((v=v-1))
      done
   fi
}

function test_num_range {
   num_range 1 3 | egrep "1|2|3" | assert_lc 3
   num_range 1 3 | head -1 | assert_eq 1
   num_range -1 1 | head -1 | assert_eq "-1"
   num_range 3 1 | egrep "1|2|3" | assert_lc 3
   num_range 3 1 | head -1 | assert_eq 3
   num_range 1 -1 | tail -1 | assert_eq "-1"
}