Как мне перебрать диапазон чисел в 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}
Связанные обсуждения: bash for loop: диапазон чисел и unix.stackexchange.com - можно ли в bash использовать целочисленную переменную в управлении циклом цикла for?
Я не помню, с какой именно версии Bash, но эта команда также поддерживает конечные нули. Что иногда действительно помогает. Команда for i in {01..10}; do echo $i; done выдаст числа вроде 01, 02, 03, ..., 10.
Для тех, кто, как я, просто хочет перебрать диапазон индексов множество, способ bash будет: myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done (обратите внимание на восклицательный знак). Он более конкретен, чем исходный вопрос, но может помочь. См. расширения параметров bash
Расширение фигурных скобок также используется для таких выражений, как {jpg,png,gif}, которые здесь не рассматриваются напрямую, хотя ответ будет идентичным. См. Раскрытие скобок с переменной? [дубликат], который отмечен как дубликат этого.
Freebsd, похоже, имеет seq "в наши дни" FWIW ...





Вы можете использовать
for i in $(seq $END); do echo $i; done
seq включает выполнение внешней команды, которая обычно замедляет работу.
Он не требует выполнения внешней команды для каждой итерации, только один раз. Если время для запуска одной внешней команды является проблемой, вы используете неправильный язык.
Так что это единственный случай, когда это имеет значение - вложение? Мне было интересно, есть ли разница в производительности или какой-то неизвестный технический побочный эффект?
@Squeaky Это отдельный вопрос, ответ на который здесь: stackoverflow.com/questions/4708549/…
Я включил этот ответ в свой ответ о сравнении производительности ниже. stackoverflow.com/a/54770805/117471 (Это примечание ко мне, чтобы отслеживать, какие из них мне осталось сделать.)
for i in $(seq 1 $END); do echo $i; doneedit: я предпочитаю seq другим методам, потому что я действительно могу его запомнить;)
seq включает выполнение внешней команды, которая обычно замедляет работу. Это может не иметь значения, но становится важным, если вы пишете сценарий для обработки большого количества данных.
Просто отлично для однострочника. Решение Pax тоже хорошее, но если бы производительность действительно была проблемой, я бы не стал использовать сценарий оболочки.
Хорошая точка зрения. Я обнаружил, что создаю быстрый и грязный сценарий ksh, который в конечном итоге превратился в монстра, и я не хотел переписывать его, учитывая время, которое я уже потратил, но это, вероятно, только я.
seq вызывается только один раз для генерации чисел. exec (), это не должно иметь значения, если этот цикл не находится внутри другого жесткого цикла.
Внешняя команда на самом деле неуместна: если вас беспокоят накладные расходы на выполнение внешних команд, вы вообще не хотите использовать сценарии оболочки, но обычно в unix накладные расходы низкие. Однако при высоком уровне END возникает проблема использования памяти.
Обратите внимание, что seq $END будет достаточно, поскольку по умолчанию начинается с 1. Из man seq: «Если FIRST или INCREMENT опущены, по умолчанию используется 1».
seq выводит число в формате с плавающей запятой. для большого количества это может сильно раздражать ...
И если вам нужны форматированные числа (например, 001, 002 и т. д.), Вы можете использовать параметр -f для seq: $ (seq -f% 03.0f 1100)
Имейте в виду, что в bash {4..1} дает 4 3 2 1, поскольку отрицательное «приращение» выводится автоматически. И наоборот, seq 4 1 ничего не дает, потому что приращение по умолчанию - 1. В таких случаях следует использовать полную команду: seq 4 -1 1.
Как упоминалось в @tzot, это может вызвать проблемы с памятью, если END большой.
СПАСИБО ТЕБЕ ЗА ЭТО!! Изменился ли синтаксис? Я думал, что диапазон определен: 'seq 1 <whatever_number>';
Это отлично работает в bash:
END=5
i=1 ; while [[ $i -le $END ]] ; do
echo $i
((i = i + 1))
done
echo $((i++)) работает и объединяет в одну линию.
У этого есть ненужные расширения bash. Версия POSIX: stackoverflow.com/a/31365662/895245
@Ciro, поскольку в вопросе конкретно говорится о bash и есть тег bash, я думаю, вы, вероятно, обнаружите, что `` расширения '' bash более чем в порядке :-)
@paxdiablo Я не имею в виду, что это неправильно, но почему бы не быть переносимым, когда мы можем ;-)
В bash мы можем просто сделать while [[ i++ -le "$END" ]]; do, чтобы выполнить (пост-) приращение в тесте.
В bash вы могли бы еще проще сделать while (( i++ < END )); do; почти нет причин использовать [[ ... ]] для арифметических выражений или сравнений.
Я включил этот ответ в свой ответ о сравнении производительности ниже. stackoverflow.com/a/54770805/117471 (Это примечание ко мне, чтобы отслеживать, какие из них мне осталось сделать.)
Как предложил Джиааро, использование 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++)).
эфемерный научил меня кое-чему: Bash допускает конструкции 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 отметил, что нотация {а..б} Bash работает только с литералами; true, согласно руководству Bash. Это препятствие можно преодолеть с помощью одного (внутреннего) fork() без exec() (как в случае с вызовом seq, который является другим образом требует fork + exec):
for i in $(eval echo "{1..$END}"); do
И eval, и echo являются встроенными командами Bash, но для подстановки команд требуется fork() (конструкция $(…)).
Единственный недостаток цикла в стиле C в том, что он не может использовать аргументы командной строки, поскольку они начинаются с символа «$».
@karatedog: for ((i=;i<=;++i)); do echo $i; done в сценарии у меня отлично работает на bash v.4.1.9, поэтому я не вижу проблем с аргументами командной строки. Вы имеете в виду что-то другое?
Кажется, что решение eval быстрее, чем встроенное в C-подобное для: $ time for ((i = 1; i <= 100000; ++ i)); делать :; done real 0m21.220s user 0m19.763s sys 0m1.203s $ time for i in $ (eval echo "{1..100000}"); делать :; Готово; реальный 0m13.881s пользователь 0m13.536s sys 0m0.152s
Да, но eval - это зло ... @MarcinZaluski time for i in $(seq 100000); do :; done намного быстрее!
Производительность должна зависеть от платформы, так как версия eval самая быстрая на моей машине.
Метод 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 рассматривает их как арифметические.
Постоянно учишься. Хотя я бы добавил typeset -i i END. Во времена, предшествующие bash (т.е. ksh), это имело значение, но тогда компьютеры были намного медленнее.
Таким образом можно избежать накладных расходов на память, связанных с большим списком, и зависимости от seq. Используй это!
Это не будет работать в Ubuntu из-за синтаксической ошибки в вашем коде.
@MarinSagovac Этот делает работает, и нет никаких синтаксических ошибок. Вы уверены, что ваша оболочка - Bash?
Расширение bash, отличное от POSIX.
@MarinSagovac Убедитесь, что #!/bin/bash является первой строкой вашего скрипта. wiki.ubuntu.com/…
просто очень короткий вопрос по этому поводу: почему ((i = 1; i <= END; i ++)) AND NOT ((i = 1; i <= $ END; i ++)); почему нет $ до END?
@Baedsch: по той же причине i не используется как $ i. На странице руководства bash для арифметической оценки указано: «В выражении на переменные оболочки также можно ссылаться по имени без использования синтаксиса раскрытия параметров».
Я включил этот ответ в свой ответ о сравнении производительности ниже. stackoverflow.com/a/54770805/117471 (Это примечание для меня, чтобы отслеживать, какие из них мне осталось сделать.) Я создал этот ответ специально для рассмотрения безоговорочного утверждения @bobbogo и тех, кто его поддержал. СПОЙЛЕР: Накладные расходы на память для большого списка не так страшны, как низкая производительность циклов в стиле c в bash. Прокомментируйте там, если у вас есть мысли. Давайте не будем перехватывать эту ветку.
Еще один уровень косвенности:
for i in $(eval echo {1..$END}); do
∶
+1: Также eval 'for i in {1 ..' $ END '}; do ... 'eval кажется естественным способом решить эту проблему.
Если вы используете BSD / OS X, вы можете использовать jot вместо seq:
for i in $(jot $END); do echo $i; done
Вот почему исходное выражение не сработало.
От мужик:
Brace expansion is performed before any other expansions, and any characters special to other expansions are preserved in the result. It is strictly textual. Bash does not apply any syntactic interpretation to the context of the expansion or the text between the braces.
Итак, расширение скобки - это то, что делается на ранней стадии чисто текстовой макрооперации, до расширение параметра.
Оболочки - это высокооптимизированные гибриды макропроцессоров и более формальных языков программирования. Чтобы оптимизировать типичные варианты использования, язык стал более сложным, и были приняты некоторые ограничения.
Recommendation
Я бы посоветовал придерживаться функций 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
У меня нет возможностей, но я бы переместил это немного вверх по списку, прежде всего, глядя на пупок bash, но сразу после цикла for в стиле C и арифметической оценки.
Подразумевается, что расширение скобок не экономит много памяти по сравнению с seq для больших диапазонов. Например, echo {1..1000000} | wc показывает, что эхо производит 1 строку, миллион слов и 6 888 896 байт. Попытка seq 1 1000000 | wc дает миллион строк, миллион слов и 6 888 896 байт, а также более чем в семь раз быстрее, по данным команды time.
Примечание: я уже упоминал метод POSIX while в своем ответе: stackoverflow.com/a/31365662/895245 Но рад, что вы согласны :-)
Я включил этот ответ в свой ответ о сравнении производительности ниже. stackoverflow.com/a/54770805/117471 (Это примечание ко мне, чтобы отслеживать, какие из них мне осталось сделать.)
@mateor Я думал, что цикл C-стиль и арифметическая оценка - это одно и то же решение. Я что-то упускаю?
Мне нравится либо цикл for в стиле C, либо while. Одна особенность, отказавшаяся от расширения диапазона bash, - это заполнение нулями с помощью {01..99}. Однако printf (1) может делать то же самое. Вместо echo попробуйте printf "%02i\n" $i в цикле for. Попробуйте printf "%02i\n" $((i++)) для метода цикла while. Строка формата: «0» обозначает площадку с нулем и пробелом, «2» - ширину поля, «i» - целое число, и не забывайте о новой строке :)
Все они хороши, но 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, поэтому он должен быть подходящим для интенсивных операций.
Я знаю, что этот вопрос касается 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
Я включил этот ответ в свой ответ о сравнении производительности ниже. stackoverflow.com/a/54770805/117471 (Это примечание ко мне, чтобы отслеживать, какие из них мне осталось сделать.)
Путь POSIX
Если вы заботитесь о переносимости, используйте пример из стандарта POSIX:
i=2
end=5
while [ $i -le $end ]; do
echo $i
i=$(($i+1))
done
Выход:
2
3
4
5
Что такое нет POSIX:
(( )) без доллара, хотя это обычное расширение как упоминается самим POSIX.[[. [ здесь достаточно. См. Также: В чем разница между одинарными и двойными квадратными скобками в Bash?for ((;;))seq (GNU Coreutils){start..end}, и это не может работать с переменными, как упоминалось по руководству Bash.let i=i+1: POSIX 7 2. Язык команд оболочки не содержит слова let и не работает на bash --posix 4.3.42доллар по i=$i+1 может потребоваться, но я не уверен. POSIX 7 2.6.4 Расширение арифметики говорит:
If the shell variable x contains a value that forms a valid integer constant, optionally including a leading plus or minus sign, then the arithmetic expansions "$((x))" and "$(($x))" shall return the same value.
но если читать буквально, это не означает, что $((x+1)) расширяется, поскольку x+1 не является переменной.
Только что получил 4 положительных голоса за этот ответ, что очень необычно. Если это было размещено на каком-то сайте с агрегацией ссылок, пожалуйста, дайте мне ссылку, ура.
Цитата относится к x, а не ко всему выражению. $((x + 1)) в порядке.
Хотя он не является переносимым и отличается от GNU seq (BSD seq позволяет вам установить строку завершения последовательности с помощью -t), FreeBSD и NetBSD также имеют seq начиная с 9.0 и 3.0, соответственно.
@CiroSantilli @chepner $((x+1)) и $((x + 1)) анализируют точно так же, так как когда парсер токенизирует x+1, он будет разделен на 3 токена: x, + и 1. x не является допустимым числовым токеном, но является допустимым токеном имени переменной, а x+ - нет, отсюда и разделение. + является допустимым токеном арифметического оператора, а +1 - нет, поэтому токен снова разделяется там. И так далее.
Я включил этот ответ в свой ответ о сравнении производительности ниже. stackoverflow.com/a/54770805/117471 (Это примечание ко мне, чтобы отслеживать, какие из них мне осталось сделать.)
Это еще один способ:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Это накладные расходы на порождение другой оболочки.
На самом деле, это ужасно, потому что порождает 2 снаряда, хотя одного достаточно.
Если вы выполняете команды оболочки и у вас (как и у меня) есть фетиш на конвейерную обработку, это хорошо:
seq 1 $END | xargs -I {} echo {}
Если вам нужен префикс, то вам может понравиться этот
for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done
что даст
07
08
09
10
11
12
Разве printf "%02d\n" $i не был бы проще, чем printf "%2.0d\n" $i |sed "s/ /0/"?
Если вы хотите максимально приблизиться к синтаксису выражения фигурных скобок, попробуйте Функция range из range.bash от bash-tricks.
Например, все следующие действия будут делать то же самое, что и 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.{a..z}; скобка расширение будет. Однако вопрос касался диапазонов числа, так что это придирка.{1..10} с расширенными скобками, поэтому программы, которые используют оба, могут быть немного сложнее для чтения.$END не является допустимым диапазоном "bookend" для другой стороны диапазона. Если, например, END=a, ошибки не произойдет, и будет отображено дословное значение {1..a}. Это поведение по умолчанию и для Bash - просто часто бывает неожиданным.Отказ от ответственности: я являюсь автором связанного кода.
Это работает в 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=
e=
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"
}
Я объединил здесь несколько идей и измерил производительность.
seq и {..} действительно быстрыеfor и while медленные$( ) медленныйfor (( ; ; )) медленнее$(( )) еще медленнееЭто не выводы. Чтобы сделать выводы, вам нужно будет взглянуть на код 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
Хороший! Однако не согласен с вашим резюме. Мне кажется, что скорость $(seq) примерно такая же, как у {a..b}. Кроме того, каждая операция занимает примерно одинаковое время, поэтому для меня добавляется около 4 мкс на каждую итерацию цикла. Здесь операция - это эхо в теле, арифметическое сравнение, приращение и т. д. Удивительно ли что-нибудь из этого? Кого волнует, сколько времени требуется атрибутам цикла, чтобы выполнить свою работу - время выполнения, вероятно, будет зависеть от содержимого цикла.
@bobbogo, вы правы, дело действительно в количестве операций. Я обновил свой ответ, чтобы отразить это. Многие выполняемые нами вызовы на самом деле выполняют больше операций, чем мы могли бы ожидать. Я сузил его из списка примерно из 50 выполненных мною тестов. Я ожидал, что мое исследование будет слишком занудным даже для этой толпы. Как всегда, я предлагаю расставить приоритеты при написании кода следующим образом: сделайте его короче; Сделайте его читабельным; Сделайте это быстрее; Сделайте его портативным. Часто №1 вызывает №3. Не тратьте время на №4, пока это не понадобится.
Это интересное упражнение, хотя начальный вопрос касается использования итерации с переменным счетчиком, что, например, не позволяет {..}.
Есть много способов сделать это, но те, которые я предпочитаю, приведены ниже.
seqSynopsis from
man seq
$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last
Syntax
Полная команда seq first incr last
Пример:
$ 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}Здесь first и last являются обязательными, а 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
если вы не хотите использовать «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
Привет всем, информация и советы, которые я здесь прочитал, действительно полезны. Я думаю, что лучше избегать использования seq. Причина в том, что некоторые сценарии должны быть переносимыми и должны работать в самых разных системах unix, где некоторые команды могут отсутствовать. В качестве примера, seq по умолчанию отсутствует в системах FreeBSD.