it-swarm.xyz

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

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

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

1441
Jiaaro

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

368
ephemient

обсуждение

Использование seq хорошо, как предложил Jiaaro. 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++)).

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

Так,

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

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

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

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

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

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

172
tzot

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

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

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

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

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

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

Я бы предложил придерживаться 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, и тогда есть все обычные аргументы, соответствующие стандартам. Помните Shellshock, aka bashdoor?
95
DigitalRoss

способ 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
    ∶
31
bobbogo

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

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

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

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

это даст

07
08
09
10
11
12
18
hossbear

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

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

Это прекрасно работает в bash:

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

Если вы хотите максимально приблизиться к синтаксису фигурных выражений, попробуйте воспользоваться функцией 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, которое часто бывает неожиданным.

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

7
Zac B

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

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

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

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

tmpstart=0;
tmpend=4;

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

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

0
1
2
3
4
6
BashTheKeyboard

Это все хорошо, но 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, поэтому он должен подходить для интенсивных операций.

6
SuperBob

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

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
6
Bruno Bronosky

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

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

5
Alex Spangher

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

Использование 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
0
theBuzzyCoder

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

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"
}
0
Ethan Post

если вы не хотите использовать '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

0
Zimba