Как перебрать диапазон чисел в 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}
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))
, Bash рассматривает их как арифметику.
Использование 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()
(конструкция $(…)
).
Вот почему оригинальное выражение не сработало.
От человек Баш:
Раскладка скобок выполняется перед любыми другими расширениями, и любые символы, особенные для других расширений, сохраняются в результате. Это строго по тексту. 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
способ 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 Арифметическое расширение говорит:
Если переменная Shell x содержит значение, которое формирует действительную целочисленную константу, необязательно включающую в себя ведущий знак плюс или минус, то арифметические расширения "$ ((x))" и "$ (($ x))" должны возвращать то же самое значение.
но, читая его буквально, это не означает, что $((x+1))
расширяется, поскольку x+1
не является переменной.
Еще один слой косвенности:
for i in $(eval echo {1..$END}); do
∶
Ты можешь использовать
for i in $(seq $END); do echo $i; done
Если вам нужен префикс, чем это может вам понравиться
for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done
это даст
07
08
09
10
11
12
Если вы используете 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
Если вы хотите максимально приблизиться к синтаксису фигурных выражений, попробуйте воспользоваться функцией 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.{a..z}
; скобка расширения будет. Вопрос был о диапазонах чисел , так что это обман.{1..10}
, поэтому программы, которые используют оба, могут быть немного сложнее для чтения.$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}
Это еще один способ:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Замените {}
на (( ))
:
tmpstart=0;
tmpend=4;
for (( i=$tmpstart; i<=$tmpend; i++ )) ; do
echo $i ;
done
Урожайность:
0
1
2
3
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, поэтому он должен подходить для интенсивных операций.
Я объединил несколько идей здесь и измерил производительность.
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
Если вы выполняете команды Shell и у вас (как и у меня) есть фетиш для конвейерной обработки, это хорошо:
seq 1 $END | xargs -I {} echo {}
Есть много способов сделать это, однако те, которые я предпочитаю, даны ниже
seq
Синопсис от
man seq
$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last
Синтаксис
Полная команда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}
Здесь первый и последний являются обязательными, а 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
Это работает в Баш и Корне, также может идти от более высоких к более низким числам. Вероятно, не самый быстрый или самый красивый, но работает достаточно хорошо. Обрабатывает негативы тоже.
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"
}
если вы не хотите использовать '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