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

1073

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

  • 21
    Привет всем, информация и подсказки, которые я прочитал здесь, действительно полезны. Я думаю, что лучше избегать использования seq. Причина в том, что некоторые сценарии должны быть переносимыми и запускаться в самых разных системах Unix, где некоторые команды могут отсутствовать. Просто для примера, seq по умолчанию отсутствует в системах FreeBSD.
Показать ещё 3 комментария
Теги:
for-loop
syntax

17 ответов

1206
Лучший ответ
for i in $(seq 1 $END); do echo $i; done

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

  • 29
    seq включает выполнение внешней команды, которая обычно замедляет работу. Это может не иметь значения, но становится важным, если вы пишете сценарий для обработки большого количества данных.
  • 31
    Просто отлично для одного лайнера. Решение Pax тоже хорошо, но если бы производительность действительно была проблемой, я бы не использовал сценарий оболочки.
Показать ещё 9 комментариев
301

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

  • 1
    Один постоянно учится. Я бы typeset -ii END . Во времена до bash (то есть ksh) это имело значение, но компьютеры были намного медленнее.
  • 44
    Этот способ позволяет избежать накладных расходов памяти большого списка и зависимости от seq . Используй это!
Показать ещё 8 комментариев
153

Обсуждение

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

  • 1
    Единственный недостаток цикла стиля C в том, что он не может использовать аргументы командной строки, так как они начинаются с «$».
  • 3
    @karatedog: for ((i=$1;i<=$2;++i)); do echo $i; done в скрипте отлично работает для меня на баш v.4.1.9, так что я не вижу проблемы с аргументами командной строки. Вы имеете в виду что-то еще?
Показать ещё 3 комментария
86

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

От человека 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?
  • 13
    У меня нет силы, но я бы переместил это немного вверх по списку, прежде всего, к пристальному взгляду пупка, но сразу после стиля C для цикла и арифметической оценки.
  • 2
    Подразумевается, что расширение фигурных скобок не экономит много памяти по сравнению с seq для больших диапазонов. Например, echo {1..1000000} | wc показывает, что эхо выдает 1 строку, миллион слов и 6 888 896 байт. Try seq 1 1000000 | wc дает миллион строк, миллион слов и 6888896 байт, а также более чем в семь раз быстрее, чем измеряется командой time .
Показать ещё 2 комментария
46

Способ POSIX

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

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

Вывод:

2
3
4
5

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

  • 0
    Только 4 ответа на этот ответ, что весьма необычно. Если это было размещено на каком-либо сайте агрегации ссылок, пожалуйста, дайте мне ссылку, ура.
  • 0
    Цитата относится к x , а не ко всему выражению. $((x + 1)) просто отлично.
Показать ещё 3 комментария
28

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

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

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

for i in $(seq $END); do echo $i; done
  • 0
    seq включает выполнение внешней команды, которая обычно замедляет работу.
  • 7
    Он не включает выполнение внешней команды для каждой итерации, только один раз. Если время для запуска одной внешней команды является проблемой, вы используете неправильный язык.
Показать ещё 6 комментариев
18

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

for i in $(jot $END); do echo $i; done
  • 0
    Это не работает на 10.8.x
  • 0
    Используя 10.8.5 здесь, и он работает нормально.
Показать ещё 1 комментарий
13

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

END=5
i=1 ; while [[ $i -le $END ]] ; do
    echo $i
    ((i = i + 1))
done
  • 14
    ((i++)) работает
  • 4
    echo $((i++)) работает и объединяет его в одну строку.
Показать ещё 6 комментариев
11

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

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

что даст

07
08
09
10
11
12
  • 3
    Не будет ли printf "%02d\n" $i проще, чем printf "%2.0d\n" $i |sed "s/ /0/" ?
6

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

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

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

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

tmpstart=0;
tmpend=4;

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

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

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

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

3

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

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

2

Если вы хотите как можно ближе подойти к синтаксису brace-expression, попробуйте 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), предотвращается как хорошо.

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

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

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

0

Это работает в 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"
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню