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

18 ответов

Решение
for i in $(seq 1 $END); do echo $i; done

редактировать: я предпочитаю 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)) случаи, Баш рассматривает их как арифметику.

обсуждение

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

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

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

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

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

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

Так,

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

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

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

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

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

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

Вот почему оригинальное выражение не сработало.

От человека Баш:

Раскладка скобок выполняется перед любыми другими расширениями, и любые символы, особенные для других расширений, сохраняются в результате. Это строго текстуально. 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, и тогда есть все обычные аргументы в поддержку стандартов. Помните ракушку, она же Башдур?

Способ 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 $(seq $END); do echo $i; done

Еще один слой косвенности:

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

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

TL; DR на вынос:

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

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

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

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

это даст

07
08
09
10
11
12

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

С помощью 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

Если вы используете 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

Это еще один способ:

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

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

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

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

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

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

Замещать {} с (( )):

tmpstart=0;
tmpend=4;

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

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

0
1
2
3
4

Все это хорошо, но 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

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

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

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

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

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

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

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

PS: у моего bash нет 'seq'в любом случае.

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

Это работает в Баш и Корне, также может идти от более высоких к более низким числам. Вероятно, не самый быстрый или самый красивый, но работает достаточно хорошо. Обрабатывает негативы тоже.

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"
}
Другие вопросы по тегам