Как перебирать диапазон чисел, определяемых переменными в 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}

Ответы

Ответ 1

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

edit: Я предпочитаю seq по другим методам, потому что я действительно могу его запомнить;)

Ответ 2

Метод 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 рассматривает их как арифметику.

Ответ 3

Обсуждение

Использование seq отлично, как предложил Jiaaro. 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 научил меня чему-то: Bash допускает конструкцию for ((expr;expr;expr)). Поскольку я никогда не читал всю страницу man для Bash (например, я сделал с man-страницей оболочки Korn (ksh), и это было давно), я пропустил это.

Итак,

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

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

начальный вопрос

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

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

Оба eval и echo являются Bash встроенными, но для подстановки команд (конструкцией $(…)) требуется fork()).

Ответ 4

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

От человека bash:

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

Итак, расширение расширений - это нечто, что делается раньше, как чисто текстовая операция макроса, перед расширением параметра.

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

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

Я предлагаю придерживаться функций Posix 1. Это означает использование 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. Помните shellshock, иначе bashdoor?

Ответ 5

Способ POSIX

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

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

Вывод:

2
3
4
5

Вещи, которые не являются POSIX:

Ответ 6

Другой слой косвенности:

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

Ответ 7

Вы можете использовать

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

Ответ 8

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

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

Ответ 9

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

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

что даст

07
08
09
10
11
12

Ответ 10

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

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

Ответ 11

Я знаю, что этот вопрос касается 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}

Ответ 12

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

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

Ответ 13

Если вы хотите оставаться как можно ближе к синтаксису фигурных выражений, попробуйте функцию 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 с минимальным количеством возможных ошибок: поддерживаются не только переменные, но и часто нежелательное поведение недопустимых диапазонов, представляемых в виде строк (например, for я in {1..a}; do echo $i; done) также предотвращается.

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

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

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

Ответ 14

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

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

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

Этот вывод также может быть присвоен переменной:

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

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

Ответ 15

Замените {} на (( )):

tmpstart=0;
tmpend=4;

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

Урожайность:

0
1
2
3
4

Ответ 16

Я объединил несколько идей здесь и измерил производительность.

TL; DR на вынос:

  1. seq и {..} действительно быстрые
  2. циклы for и в while как медленные
  3. $( ) медленно
  4. for ((; ; )) циклы медленнее
  5. $(( )) еще медленнее
  6. Беспокойство по поводу N чисел в памяти (seq или {..}) глупо (по крайней мере, до 1 млн.)

Это не выводы. Вам придется взглянуть на код C за каждым из них, чтобы сделать выводы. Это больше о том, как мы склонны использовать каждый из этих механизмов для зацикливания кода. Большинство отдельных операций достаточно близки к той же скорости, что в большинстве случаев не имеют значения. Но механизм, подобный for (( i=1; i<=1000000; i++ )) - это множество операций, которые вы можете увидеть визуально. Это также намного больше операций за цикл, чем вы получаете for я in $(seq 1 1000000). И это может быть неочевидным для вас, поэтому проведение подобных тестов является полезным.

демос

# show that seq is fast
$ time (seq 1 1000000 | wc)
 1000000 1000000 6888894

real    0m0.227s
user    0m0.239s
sys     0m0.008s

# show that {..} is fast
$ time (echo {1..1000000} | wc)
       1 1000000 6888896

real    0m1.778s
user    0m1.735s
sys     0m0.072s

# Show that for loops (even with a : noop) are slow
$ time (for i in {1..1000000} ; do :; done | wc)
       0       0       0

real    0m3.642s
user    0m3.582s
sys 0m0.057s

# show that echo is slow
$ time (for i in {1..1000000} ; do echo $i; done | wc)
 1000000 1000000 6888896

real    0m7.480s
user    0m6.803s
sys     0m2.580s

$ time (for i in $(seq 1 1000000) ; do echo $i; done | wc)
 1000000 1000000 6888894

real    0m7.029s
user    0m6.335s
sys     0m2.666s

# show that C-style for loops are slower
$ time (for (( i=1; i<=1000000; i++ )) ; do echo $i; done | wc)
 1000000 1000000 6888896

real    0m12.391s
user    0m11.069s
sys     0m3.437s

# show that arithmetic expansion is even slower
$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; i=$(($i+1)); done | wc)
 1000000 1000000 6888896

real    0m19.696s
user    0m18.017s
sys     0m3.806s

$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; ((i=i+1)); done | wc)
 1000000 1000000 6888896

real    0m18.629s
user    0m16.843s
sys     0m3.936s

$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $((i++)); done | wc)
 1000000 1000000 6888896

real    0m17.012s
user    0m15.319s
sys     0m3.906s

# even a noop is slow
$ time (i=1; e=1000000; while [ $((i++)) -le $e ]; do :; done | wc)
       0       0       0

real    0m12.679s
user    0m11.658s
sys 0m1.004s

Ответ 17

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

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

Ответ 18

Это работает в 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=${1}
   e=${2}
   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"
}

Ответ 19

Есть много способов сделать это, однако те, которые я предпочитаю, даны ниже

Использование seq

Синопсис от man seq

$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last

Синтаксис

Полная команда
seq first incr last

  • first - начальный номер в последовательности [необязательно, по умолчанию: 1]
  • incr - инкремент [необязательно, по умолчанию: 1]
  • последний последний номер в последовательности

Пример:

$ seq 1 2 10
1 3 5 7 9

Только с первым и последним:

$ seq 1 5
1 2 3 4 5

Только с последнего:

$ seq 5
1 2 3 4 5

Используя {first..last..incr}

Здесь первый и последний являются обязательными, а incr является необязательным

Используя только первый и последний

$ echo {1..5}
1 2 3 4 5

Использование incr

$ echo {1..10..2}
1 3 5 7 9

Вы можете использовать это даже для символов, как показано ниже

$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z

Ответ 20

если вы не хотите использовать ' seq ' или ' eval ' или jot или арифметический формат расширения, например. for ((i=1;i<=END;i++)) или других циклов, например. while вы не хотите ' printf ' и рады только ' echo ', тогда этот простой обходной путь может соответствовать вашему бюджету:

a=1; b=5; d='for я in {'$a'..'$b'}; do echo -n "$i"; done;' echo "$d" | bash

PS: Мой bash не имеет команды ' seq ' в любом случае.

Протестировано на Mac OSX 10.6.8, Bash 3.2.48