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

1545

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

eschercycle
источник
26
Привет всем, информация и подсказки, которые я прочитал здесь, действительно полезны. Я думаю, что лучше избегать использования seq. Причина в том, что некоторые сценарии должны быть переносимыми и запускаться в самых разных системах Unix, где некоторые команды могут отсутствовать. Просто для примера, seq по умолчанию отсутствует в системах FreeBSD.
9
Я не помню, с какой именно версии Bash, но эта команда также поддерживает конечные нули. Что иногда очень полезно. Команда for i in {01..10}; do echo $i; doneвыдаст такие цифры, как 01, 02, 03, ..., 10.
Topr
1
Для таких как я, которые просто хотят перебрать диапазон индексов массива , способ bash будет: myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done(обратите внимание на восклицательный знак). Это более конкретный вопрос, чем первоначальный вопрос, но может помочь. Смотрите расширения параметров bash
PlasmaBinturong
1
Расширение скобок также используется для таких выражений, {jpg,png,gif}которые прямо не рассматриваются, хотя ответ будет идентичным. Видите расширение скобки с переменной? [дубликат], который помечен как дубликат этого.
tripleee

Ответы:

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

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

Jiaaro
источник
36
seq включает выполнение внешней команды, которая обычно замедляет работу. Это может не иметь значения, но становится важным, если вы пишете сценарий для обработки большого количества данных.
paxdiablo
37
Просто отлично для одного лайнера. Решение Pax тоже хорошо, но если бы производительность действительно была проблемой, я бы не использовал сценарий оболочки.
eschercycle
17
seq вызывается только один раз для генерации чисел. exec () это не должно быть значительным, если этот цикл не находится внутри другого узкого цикла.
Хавьер
29
Внешняя команда на самом деле не подходит: если вы беспокоитесь о накладных расходах на запуск внешних команд, то вы вообще не хотите использовать сценарии оболочки, но, как правило, в unix накладные расходы малы. Тем не менее, существует проблема использования памяти, если END высокий.
Марк Бейкер
18
Обратите внимание, что этого seq $ENDбудет достаточно, поскольку по умолчанию начинается с 1. From man seq: «Если FIRST или INCREMENT опущены, по умолчанию используется значение 1».
Федорки 'ТАК прекрати вредить'
475

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)в С и других подобных языках, как и другие ((expr))случаи, Bash рассматривает их как арифметика.

ephemient
источник
68
Этот способ позволяет избежать затрат памяти большого списка и зависимости от него seq. Используй это!
Боббого
3
@MarinSagovac Это делает работу , и нет никаких синтаксических ошибок. Вы уверены, что ваша оболочка Bash?
gniourf_gniourf
3
@MarinSagovac Обязательно сделайте #!/bin/bashпервую строчку своего скрипта. wiki.ubuntu.com/...
Melebius
7
просто очень короткий вопрос: почему ((i = 1; i <= END; i ++)) и NOT ((i = 1; i <= $ END; i ++)); почему нет $ до конца?
Baedsch
5
@Baedsch: по той же причине я не используется как $ i. Страница состояния bash для арифметической оценки: «Внутри выражения на переменные оболочки также можно ссылаться по имени без использования синтаксиса расширения параметров».
user3188140
193

обсуждение

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

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

Так,

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()для подстановки команд ( $(…)конструкция) требуется .

tzot
источник
1
Единственный недостаток цикла стиля C - он не может использовать аргументы командной строки, так как они начинаются с «$».
каратэдог
3
@karatedog: for ((i=$1;i<=$2;++i)); do echo $i; doneв скрипте у меня отлично работает на bash v.4.1.9, поэтому я не вижу проблемы с аргументами командной строки. Вы имеете в виду что-то еще?
tzot
Кажется, что eval решение быстрее, чем встроенное в C-like для: $ time for ((i = 1; i <= 100000; ++ i)); делать :; выполнено в реальном времени 0m21.220s пользователь 0m19.763s sys 0m1.203s $ время для i in $ (eval echo "{1..100000}"); делать :; сделанный; реальный 0m13.881s пользователь 0m13.536s sys 0m0.152s
Марцин Залуски
3
Да, но eval - это зло ... @MarcinZaluski time for i in $(seq 100000); do :; doneнамного быстрее!
Ф. Хаури
Производительность должна зависеть от платформы, так как eval-версия является самой быстрой на моей машине.
Эндрю Прок
103

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

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

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

DigitalRoss
источник
13
У меня нет силы, но я бы переместил это немного вверх по списку, прежде всего, к пристальному взгляду пупка, но сразу после C-стиля для цикла и арифметической оценки.
Метеор
2
Подразумевается, что расширение фигурных скобок не экономит много памяти по сравнению с seqбольшими диапазонами. Например, echo {1..1000000} | wcпоказывает, что эхо производит 1 строку, миллион слов и 6 888 896 байт. Попытка seq 1 1000000 | wcдает миллион строк, миллион слов и 6888896 байт, а также более чем в семь раз быстрее, чем измеряется timeкомандой.
Джордж
Примечание. whileРанее я уже упоминал метод POSIX в своем ответе: stackoverflow.com/a/31365662/895245 Но рад, что вы согласны :-)
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
Я включил этот ответ в свой ответ сравнения производительности ниже. stackoverflow.com/a/54770805/117471 (Это записка для меня, чтобы отследить, какие из них я оставил делать.)
Бруно Броноски
@mateor Я думал, что C-стиль для цикла и арифметической оценки - это одно и то же решение. Я что-то пропустил?
Оскар Чжан
73

Способ POSIX

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

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

Вывод:

2
3
4
5

Вещи, которые не POSIX:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
источник
Только 4 ответа на этот ответ, что весьма необычно. Если это было размещено на каком-либо сайте агрегации ссылок, пожалуйста, дайте мне ссылку, ура.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
Цитата относится xне ко всему выражению. $((x + 1))просто отлично.
Chepner
Хотя FreeBSD и NetBSD не являются переносимыми и отличаются от GNU seq(BSD seqпозволяет устанавливать строку завершения последовательности с помощью -t), они также имеют версии seq9.0 и 3.0 соответственно.
Адриан Гюнтер
@CiroSantilli @chepner $((x+1))и $((x + 1))синтаксического анализа точно так же, как и когда анализатор размечает x+1будет разделен на 3 лексем: x, +, и 1. xне является допустимым числовым токеном, но является допустимым токеном имени переменной, но не x+является, следовательно, разделением. +является допустимым токеном арифметического оператора, но +1его нет, поэтому токен снова разделен там. И так далее.
Адриан Гюнтер
Я включил этот ответ в свой ответ сравнения производительности ниже. stackoverflow.com/a/54770805/117471 (Это записка для меня, чтобы отследить, какие из них я оставил делать.)
Бруно Броноски
35

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

for i in $(eval echo {1..$END}); do
    
bobbogo
источник
2
+1: также eval 'для i в {1 ..' $ END '}; do ... 'eval кажется естественным способом решить эту проблему.
Уильям Перселл
28

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

for i in $(seq $END); do echo $i; done
Питер Хоффманн
источник
seq включает выполнение внешней команды, которая обычно замедляет работу.
paxdiablo
9
Он не включает в себя выполнение внешней команды для каждой итерации, только один раз. Если время запуска одной внешней команды является проблемой, вы используете не тот язык.
Марк Бейкер
1
Так является ли вложение единственным случаем, когда это имеет значение? Мне было интересно, была ли разница в производительности или какой-то неизвестный технический побочный эффект?
Sqeaky
@Squeaky Это отдельный вопрос, на который он ответил здесь: stackoverflow.com/questions/4708549/…
tripleee
Я включил этот ответ в свой ответ сравнения производительности ниже. stackoverflow.com/a/54770805/117471 (Это записка для меня, чтобы отследить, какие из них я оставил делать.)
Бруно Броноски
21

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

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

это даст

07
08
09
10
11
12
hossbear
источник
4
Не printf "%02d\n" $iбудет проще чем printf "%2.0d\n" $i |sed "s/ /0/"?
zb226
19

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

for i in $(jot $END); do echo $i; done
jefeveizen
источник
17

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

END=5
i=1 ; while [[ $i -le $END ]] ; do
    echo $i
    ((i = i + 1))
done
paxdiablo
источник
6
echo $((i++))работает и объединяет его в одну строку.
Бруно Броноски
1
Это лишние расширения bash. Версия POSIX: stackoverflow.com/a/31365662/895245
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
1
@Ciro, поскольку в вопросе конкретно говорится о bash и есть тег bash, я думаю, вы, вероятно, обнаружите, что «расширения» bash более чем приемлемы :-)
paxdiablo
@paxdiablo Я не имею в виду, что это не правильно, но почему бы не быть портативным, когда мы можем ;-)
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
В bash, мы можем просто while [[ i++ -le "$END" ]]; doсделать (пост-) приращение в тесте
Аарон МакДейд
14

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

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
Бруно Броноски
источник
1
Ницца! Не соглашайтесь с вашим резюме, хотя. Похоже, $(seq)это примерно с той же скоростью, что и {a..b}. Кроме того, каждая операция занимает примерно одно и то же время, поэтому для меня добавляется около 4 мкс к каждой итерации цикла. Здесь операция - это эхо в теле, арифметическое сравнение, приращение и т. Д. Удивительно ли это? Кому интересно, сколько времени понадобится атрибутике цикла, чтобы выполнить свою работу - во время выполнения, скорее всего, будет доминировать содержимое цикла.
Боббого
@ Bobbogo вы правы, это действительно счет операций. Я обновил свой ответ, чтобы отразить это. Многие звонки, которые мы делаем, на самом деле выполняют больше операций, чем мы могли бы ожидать. Я сузил это из списка около 50 тестов, которые я провел. Я ожидал, что мое исследование было слишком утомительным даже для этой толпы. Как всегда, я предлагаю расставить приоритеты в ваших усилиях по написанию кода так: сделайте это короче; Сделайте это читабельным; Сделать это быстрее; Сделайте это портативным. Часто # 1 вызывает # 3. Не тратьте свое время на # 4, пока вы не должны.
Бруно Броноски
8

Я знаю, что этот вопрос о 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}
Адриан Фрювирт
источник
8

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

end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Jahid
источник
1
Это накладные расходы на порождение другой оболочки.
codeforester
1
На самом деле, это очень ужасно, потому что порождает 2 оболочки, когда 1 будет достаточно.
Бруно Броноски
8

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

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

tmpstart=0;
tmpend=4;

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

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

0
1
2
3
4
BashTheKeyboard
источник
Я включил этот ответ в свой ответ сравнения производительности ниже. stackoverflow.com/a/54770805/117471 (Это записка для меня, чтобы отследить, какие из них я оставил делать.)
Бруно Броноски
6

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

SuperBob
источник
5

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

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

Алекс Спангер
источник
3

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

С помощью 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
theBuzzyCoder
источник
3

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

Zimba
источник
0

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

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"
}
Итан Пост
источник