Я могу перебирать несколько диапазонов/последовательностей/числа с помощью таких конструкций, как:
for i in $(seq 1 3) 5 $(seq 7 9) 11; do echo $i; done
for i in {1..3} 5 {7..9} 11; do echo $i; done
Но как добиться того же результата, если в var указаны диапазоны/числа:
var = "{1..3} 5 {7..9} 11"
Я обнаружил, что это можно сделать с помощью eval и seq:
var = "seq 1 3; echo 5; seq 7 9; echo 11"; for i in $(eval "$var"); do echo $i; done
Здесь показано, что eval echo позволяет использовать диапазоны в таких переменных, как (гораздо удобнее, чем предыдущий):
var = "{1..3} 5 {7..9} 11"; for i in $(eval echo "$var"); do echo $i; done
Можно ли как-то добиться того же результата, не используя eval? Или использовать eval в данном конкретном случае можно?
Обновлять
Поскольку существуют дополнительные накладные расходы при синтаксическом анализе диапазонов/числа, определенных в простой строковой переменной, и поскольку я могу контролировать формирование такого параметра, также стоит рассмотреть решения для входного массива var, определенного следующим образом:
ranges=( '1 3' 5 '7 9' 11 )
Или еще более прямолинейно и просто для реализации (но немного избыточно в выражении) с использованием диапазонов вместо чисел:
ranges=( '1 3' '5 5' '7 9' '11 11' )
В случае использования массива с диапазонами в качестве входной строки этот вопрос становится довольно простым и сводится к двойному циклу по массиву с диапазонами и по каждому диапазону этого массива.
При указании диапазонов в виде массива не будет необходимости использовать eval или анализировать строку с диапазонами в массив (что имеет ту же самую, но скрытую опасность eval).
Вы могли бы var = "{1..3} 5 {7..9} 11"; declare -a "array=($var)", но ваша переменная все еще eval устарела.





Вы можете использовать:
var = "{1..3} 5 {7..9} 11"
declare -a "array=($var)"
for i in "${array[@]}"; do
echo $i
done
1
2
3
5
7
8
9
11
eval:var='{1..3} $(uptime) 6 7'
echo "$var"
{1..3} $(uptime) 6 7
declare -a "array=($var)"
for i in "${array[@]}"; do
echo $i
done
1
2
3
16:36:29
up
22
days,
4:54,
23
users,
load
average:
2.48,
1.89,
1.83
6
7
Обратите внимание, что использование двойных кавычек:
var='{1..3} "$(uptime)" 6 7'
Будет более читабельно:
1
2
3
16:41:03 up 22 days, 4:59, 23 users, load average: 1.47, 2.09, 1.98
6
7
О:
Или использование eval в данном конкретном случае нормально?
Это ваша личная ответственность! Если вы знаете, что делаете, и уверены в том, откуда приходит контент, тогда все в порядке.
$var у вас будут циклы do.Если вы хотите избежать eval, вы можете использовать такую функцию, как:
readRange() { # Usage: readRange "$var" <arrayName>
local -a arin
local elem
local -i iter start end
local -n arout=$2
arout=()
read -ra arin <<< "$1"
for elem in "${arin[@]}"; do
case $elem in
'{'[0-9]*'..'*[0-9]'}' )
IFS='.{}' read -r _ start _ end <<< "$elem"
for ((iter=start; iter<=end; iter++)); do
arout+=($iter)
done
;;
*[^0-9]* )
echo ERROR 1>&2
return -1
;;
*)
arout+=($elem)
;;
esac
done
}
var = "{1..3} 5 {7..9} 11"
readRange "$var" resultArray
for i in "${resultArray[@]}"; do
echo "$i"
done
1
2
3
5
7
8
9
11
eval:readRange() { # Usage: readRange "$var" <arrayName>
local -a arin
local elem start end
local -n arout=$2
arout=()
read -ra arin <<< "$1"
for elem in "${arin[@]}"; do
read -r start end <<< ${elem//[^0-9]/ }
if [[ -n $end ]]; then
arout+=($(seq $start $end))
else
arout+=($start)
fi
done
}
var = "{1..3} 5 {7..9} 11"
readRange "$var" resultArray
for i in "${resultArray[@]}"; do
echo "$i"
done
1
2
3
5
7
8
9
11
Без eval и без каких-либо переменных, доступных оболочке для расширения, подстановки, разделения слов и т. д.:
$ cat tst.sh
#!/usr/bin/env bash
ranges=( '1 3' 5 '7 9' 11 )
for range in "${ranges[@]}"; do
printf '\nrange = %s\n' "$range"
read -r -a beg_end <<< "$range"
seq "${beg_end[0]}" "${beg_end[-1]}"
done
$ ./tst.sh
range = 1 3
1
2
3
range = 5
5
range = 7 9
7
8
9
range = 11
11
Если во входных данных было что-то, что не было допустимым диапазоном, то seq в приведенном выше примере не будет работать:
$ cat tst.sh
#!/usr/bin/env bash
ranges=( '1 3' '$(date)' '7 9' 11 )
for range in "${ranges[@]}"; do
printf '\nrange = %s\n' "$range"
read -r -a beg_end <<< "$range"
seq "${beg_end[0]}" "${beg_end[-1]}"
done
$ ./tst.sh
range = 1 3
1
2
3
range = $(date)
seq: invalid floating point argument: ‘$(date)’
Try 'seq --help' for more information.
range = 7 9
7
8
9
range = 11
11
Если вы хотите решить эту проблему каким-то другим способом, есть несколько альтернатив, но лично я бы все равно просто позволил seq быть тем, кто обнаруживает проблему, а затем просто добавил бы любую обработку, которую я хотел, например. чтобы напечатать сообщение об ошибке, а затем остановить обработку:
$ cat tst.sh
#!/usr/bin/env bash
ranges=( '1 3' '$(date)' '7 9' 11 )
for range in "${ranges[@]}"; do
printf '\nrange = %s\n' "$range"
read -r -a beg_end <<< "$range"
seq "${beg_end[0]}" "${beg_end[-1]}" 2>/dev/null || {
printf 'Bad range: "%s", exiting\n' "$range" >&2
exit 1
}
done
$ ./tst.sh
range = 1 3
1
2
3
range = $(date)
Bad range: "$(date)", exiting
Я использую '$(date)' как «плохое» значение выше, чтобы вы могли сравнить, как это решение справляется с потенциальной атакой путем внедрения команд (рассматривая его как просто буквальную строку и не выполняя его как команду) с eval или любым другим решением, которое вы можете обдумывать.
Проблема с использованием расширений фигурных скобок, таких как {1..3}, хранящихся в переменной, заключается в том, что раскрытие скобок происходит до раскрытия параметра (переменной) (см. https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions .html), поэтому невозможно var='{1..3}' расширить до 1 2 3 без двух проходов расширения с использованием eval или аналогичного, поэтому переменная расширяется до {1..3} на первом проходе перед оболочкой на втором проходе, а затем расширяет это выражение в скобках при вызове команды, которую вы хочу работать на результат. Пока вы полностью контролируете содержимое переменной, содержащей диапазон, который вы хотите расширить, eval или переменная без кавычек будет безопасной в этом конкретном случае, но нет необходимости полагаться на это, как я показываю выше.
Чтобы создать массив ranges на основе строки var: var = "'1 3' 5 '7 9' 11"; declare -a "ranges=($var)"
@AntonSamokat Я бы посоветовал вам не делать этого, поскольку это не лучше, чем использование eval, оно все равно расширит любую переменную в var, включая выполнение любой команды в ней. Попробуйте var = "'1 3' $(date) '7 9' 11"; declare -a "ranges=($var)"; declare -p ranges или даже var = "'1 3' '$(date)' '7 9' 11"; declare -a "ranges=($var)"; declare -p ranges и обратите внимание, что выходные данные содержат результат выполнения date вместо строки $(date), что является проблемой eval, которую вы пытаетесь избежать. Не пытайтесь избежать eval, используя что-то другое с той же проблемой, но с более непонятным синтаксисом.
Я добавил потенциальную атаку с внедрением команд в пример в своем ответе, чтобы вы могли увидеть, как мой код с ней справляется. У вас могут возникнуть аналогичные проблемы в других решениях с любым потенциальным расширением переменной (например, '$HOME') или подстановочными символами (например, '*'), которые мой ответ снова будет рассматривать как буквальные строки.
Я понимаю. Да, это даже хуже, чем eval из-за такой скрытой опасности, о которой известно eval. Спасибо. Оказывается, в этом случае вместо использования строковой var с диапазонами целесообразнее использовать массив var с диапазонами. И между функциями будет передаваться массив, а не строка.
Запускать seq N N для печати N — это излишне! Вместо этого вы можете использовать всего лишь один тест echo!
@F.Hauri-GiveUpGitHub, то вы добавляете тест, который будет выполняться для каждого входного «диапазона», чтобы избежать ненужного вызова seq для некоторых входных диапазонов. Это того не стоит, если нет большого количества входных диапазонов с высоким процентом одиночных значений (это может замедлить обработку, если отдельные значения встречаются редко). Как всегда, не оптимизируйте производительность до тех пор, пока у вас не появится причина и вы не поймете, какие изменения действительно помогут. В этом случае, если бы ОП решил проверить отдельные значения, это, очевидно, была бы тривиальная настройка, как вы упомянули.
Если возможно, я бы пропустил простую скалярную строковую переменную и просто использовал массив. Вы уже это делали, но если возможно, удалите жесткие строки, объявляющие диапазоны, и просто объявите диапазоны в самом определении массива.
$: lst=( {1..3} 5 {7..9} 11 )
$: printf " %s" "${lst[@]}" $'\n'
1 2 3 5 7 8 9 11
Если это не вариант, то, как уже было сказано, вы можете использовать declare -
$: var = "{1..3} 5 {7..9} 11"
$: declare -a "lst=( $var )"
$: printf " %s" "${lst[@]}" $'\n'
1 2 3 5 7 8 9 11
Но следите за вредоносными встроенными командами в ваши данные, как показано .
Если ваши последовательности невелики, вы можете хранить всю последовательность, а не только «контрольные» данные.
$: lst=( {1..3} 5 {7..9} 11 ) # create the set
$: seq = "${lst[@]}" # store as flat string
$: unset lst # delete it, or go out of scope
$: echo "[$seq][${lst[@]:-}]" # string has it, array does not
[1 2 3 5 7 8 9 11][]
$: lst=( $seq ) # re-create the array - insecure
$: for n in "${lst[@]}"; do echo $n; done
1
2
3
5
7
8
9
11
Это не решит вашу возможную проблему безопасности, если диапазоны поступают из ненадежного источника, но может упростить обработку и хранение.
Это интересный шаг вперед. Я подумал, что было бы хорошо использовать массивы массивов, чтобы исключить необходимость парсить строки с диапазонами. Но ush не поддерживает многомерные массивы . Проверю это. Спасибо!
Верно — bash не поддерживает многомерные массивы, но поддерживает ассоциативные массивы с произвольными строками в качестве ключей, которые могут быть ключами, которые вы бы использовали при многомерном поиске с любым разделителем, который не будет частью какого-либо ключа — ${table["1,3,15"]} работает просто хорошо.
Попробуйте:
var=({1..3} 5 {7..9} 11);for i in "${var[@]}";do echo $i;done