Вопрос или проблема
Существует ли случай, когда mapfile
имеет преимущества перед arr+=(input)
?
Простые примеры
имя массива mapfile, arr:
mkdir {1,2,3}
mapfile -t arr < <(ls)
declare -p arr
вывод:
declare -a arr=([0])="1" [1]="2" [2]="3")
Правка:
изменено название ниже; в теле было y
как имя массива, но в заголовке было arr
как имя, что могло бы привести к путанице.
y+=(input)
IFS=$'\n'
y+=($(ls))
declare -p y
вывод:
declare -a y=([0])="1" [1]="2" [2]="3")
Преимущество mapfile
в том, что вам не нужно беспокоиться о разбиении слов, думаю.
Другим способом вы можете избежать разбиения слов, установив IFS=$'\n'
, хотя для этого примера ничего страшного.
Второй пример кажется проще писать, может, я упускаю что-то?
Они вовсе не одинаковые, даже после IFS=$'\n'
.
Специфично для bash (хотя этот синтаксис был заимствован из zsh¹):
arr=( $(cmd) )
(arr+=( $(cmd) )
используется для добавления элементов в массив; поэтому сравнивается с keys=( -1 "${!arr[@]}" ); readarray -tO "$(( ${keys[@]: -1} + 1))" arr < <(cmd)
²).
Делает:
- Выполняет
cmd
в подпроцессе с его stdout, открытым на записывающем конце канала. - Одновременно родительский процесс оболочки читает с другого конца канала и:
- удаляет символы NUL и завершающие символы новой строки
- разделяет полученную строку на основе содержимого специальной переменной
$IFS
. Для тех символов в$IFS
, которые являются пробелами, например, новой строкой, поведение более сложное:- начальные и конечные удаляются (в случае новой строки, конечные уже удалены подстановкой команды, как показано выше)
- последовательности из одного или нескольких считаются одним разделителем. Например, вывод
printf '\n\n\na\n\n\nb\n\n\n'
разделяется на только два элемента:a
иb
.
- каждое из этих слов затем подлежит генерации имен файлов, также известной как расширение путей, чье поведение зависит от ряда опций, включая
noglob
,nullglob
,failglob
,extglob
,globasciiranges
,globstar
,nocaseglob
. Это относится к тем словам, которые содержат такие символы, как*
,?
,[
, и в некоторых версиях bash\
, и больше, еслиextglob
включен.
- Затем полученные слова присваиваются как элементы массива
$arr
.
Пример:
bash-5.1$ touch x '\x' '?x' aX $'foo\n\n\n\n*'
bash-5.1$ IFS=$'\n'
bash-5.1$ ls | cat
aX
foo
*
?x
\x
x
bash-5.1$ arr=( $(ls) )
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="aX" [3]=$'foo\n\n\n\n*' [4]="?x" [5]="\\x" [6]="x" [7]="?x" [8]="\\x" [9]="\\x" [10]="x")
Как вы видите, файл $'foo\n\n\n\n*'
был разбит на foo
и *
, и *
был расширен до списка файлов в текущем рабочем каталоге, что объясняет, почему мы получаем как foo
, так и $'foo\n\n\n\n*'
, аналогично для ?x
, что объясняет, почему мы получаем \x
(показано как "\\x"
) трижды, так как есть строка \x
в выводе ls
, и она совпадает и с *
, и с ?x
.
С bash 5.0, мы получаем:
bash-5.0$ arr=( $(ls) )
bash-5.0$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="aX" [3]=$'foo\n\n\n\n*' [4]="?x" [5]="\\x" [6]="x" [7]="?x" [8]="\\x" [9]="x" [10]="x")
С \x
только дважды, но x
трижды, так как в этой версии обратная косая черта была глобальной операцией, даже когда не следовала за другой глобальной операцией, поэтому \x
как глоб совпадает с x
.
После shopt nocaseglob
, мы получаем:
bash-5.1$ shopt -s nocaseglob
bash-5.1$ arr=( $(ls) )
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="aX" [3]=$'foo\n\n\n\n*' [4]="?x" [5]="\\x" [6]="x" [7]="aX" [8]="?x" [9]="\\x" [10]="\\x" [11]="x")
С aX
показано трижды, так как оно также совпадает с ?x
.
После shopt -s failglob
:
bash-5.0$ shopt -s failglob
bash-5.0$ arr=( $(printf '\\z\n') )
bash: no match: \z
bash-5.0$ arr=( $(printf 'WTF?') )
bash: no match: WTF?
И arr=( $(echo '/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*') )
Выходит из памяти после того, как делает вашу систему непригодной для использования в течение нескольких минут.
Итак, в общем, IFS=$'\n'; arr=( $(cmd) )
не сохраняет строки вывода cmd
в массиве, но имена файлов, полученные в результате расширения непустых строк вывода cmd
, которые обрабатываются как шаблоны.
С mapfile
или его менее вводящим в заблуждение псевдонимом readarray
:
readarray -t arr < <(cmd)
- как и выше выполняет
cmd
в подпроцессе с его stdout, открытым на записывающем конце канала. <(...)
разворачивается в нечто похожее на/dev/fd/63
или/proc/self/fd/63
, где63
— это файловый дескриптор родительской оболочки, открытый на читающем конце этого канала.- с направлением
<
коротким для0<
, этот /dev/fd/63 открывается для чтения на fd 0, что означает, что stdinreadarray
также будет читающим концом этого канала. readarray
читает каждую строку из этого канала (одновременно с записью в негоcmd
), отбрасывает разделитель строк (-t
) и сохраняет ее (до первого NUL, если содержит любой, по крайней мере в текущих версиях bash) в новом элементе массива$arr
.
Таким образом, в конце $arr
, при условии, что cmd
не выводит NUL, будет содержать содержимое каждой строки вывода cmd
, независимо от того, пустые они или нет, содержат ли они символы шаблонов или нет.
С примером выше:
bash-5.1$ readarray -t arr < <(ls)
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="" [3]="" [4]="" [5]="*" [6]="?x" [7]="\\x" [8]="x")
Это согласуется с тем, что мы видели в выводе ls | cat
ранее, но это все же неверно, если намерение состояло в том, чтобы получить список файлов в текущем рабочем каталоге. Вывод ls
невозможно обработать постфактум, если вы не используете некоторые расширения GNU, такие как --quoting-style=shell-always
или --zero
в последних версиях (9.0 и выше):
bash-5.2$ readarray -td '' arr < <(ls --zero)
bash-5.2$ typeset -p arr
declare -a arr=([0]="aX" [1]=$'foo\n\n\n\n*' [2]="?x" [3]="\\x" [4]="x")
На этот раз, readarray
сохраняет содержимое записей, разделенных NUL-символами, в $arr
. IFS=$'\0'
не может использоваться в bash
, так как bash
не может сохранять NUL в своих переменных.
Или:
bash-5.1$ eval "arr=( $(ls --quoting-style=shell-always) )"
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]=$'foo\n\n\n\n*' [2]="?x" [3]="\\x" [4]="x")
В любом случае, правильный способ получить список не скрытых файлов в текущем каталоге в массив будет следующий:
shopt -s nullglob
shopt -u failglob
arr=( * )
Вы бы прибегнули к ls --zero
только если бы вы хотели, например, чтобы список был отсортирован по размеру или времени модификации, что недоступно в bash-глобах (в отличие от zsh).
Как в:
zsh | недавние GNU bash + GNU coreutils |
---|---|
new_to_old=( *.txt(Nom) ) |
readarray -td '' new_to_old < <(ls -td --zero -- *.txt) |
four_largest=( *.txt(NOL[1,4]) ) |
readarray -td '' four_largest < <(ls -tdrS --zero -- *.txt | head -zn4) |
Еще одно отличие между a=($(cmd))
и readarray < <(cmd)
заключается в статусе выполнения, который в первом случае соответствует cmd
, а во втором – readarray
. В последних версиях bash
вы можете получить существующий статус cmd
в последнем случае с помощью wait "$!"; cmd_status=$?
.
¹ синтаксис arr=( ... )
взят из zsh (bash не имел массивов до версии 2.0 в 1996 году), но стоит отметить, что в zsh подстановка команды, хотя она также удаляет завершающие символы новой строки и подвержена удалению $IFS
, не отбрасывает NULы (NUL даже включен в значение по умолчанию $IFS
) и не подвержена глобальному расширению, как в других оболочках, подобных Bourne, что делает ее более безопасной оболочкой в целом.
² readarray
или mapfile
не имеют режима добавления, но в последних версиях вы можете указать индекс первого элемента, с которого следует начинать хранить элементы с помощью -O
, как показано здесь. Чтобы определить индекс последнего элемента в bash (где массивы редкие, как в ksh!), это чертовски сложно. Здесь, чтобы добавить строки вывода cmd
в $arr
, вместо столь сложного кода вы можете прочитать эти строки во временный массив с readarray -r tmp < <(cmd)
и добавить элементы в $arr
с arr+=( "${tmp[@]}" )
. Также стоит отметить, что если переменная arr
была объявлена как скалярная или ассоциативная, поведение будет варьироваться.
.
Ответ или решение
В мире программирования на Bash создание и добавление элементов в массивы имеет большое значение для автоматизации и управления данными. Несмотря на кажущуюся простоту, существует несколько методов, каждый из которых имеет свои особенности и применение. Рассмотрим два из них: использование команды mapfile
и оператора arr+=
.
Теория
mapfile
— это утилита, используемая для чтения строк из входного потока и сохранения их в массиве. Команда mapfile -t
считывает каждую строку входных данных, удаляет символы нового конца строки и помещает их в новый элемент массива. Это позволяет избежать лишних операций по разделению строки, так как mapfile
автоматически обрабатывает данные построчно.
В отличие от этого, выражение arr+=($(cmd))
выполняет объединение массивов, генерируя список элементов на основе выполнения команды в скобках. Этот подход склонен использовать переменную IFS
для разделения строк и потенциально может страдать от проблем, связанных с разделением слов (word splitting) и расширением глобов (globbing).
Пример
Рассмотрим два примера.
- Использование
mapfile
:
mkdir {1,2,3}
mapfile -t arr < <(ls)
declare -p arr
Этот скрипт создает три директории и с помощью mapfile
считывает их имена из вывода команды ls
, заполняя массив arr
.
- Использование
arr+=
:
IFS=$'\n'
y+=($(ls))
declare -p y
Этот скрипт делает то же самое, используя оператор добавления к массиву и переменную IFS
, чтобы разделить строки на основе новых строк.
Применение
Теперь, когда мы обсудили базовые теории и привели примеры, давайте поговорим о применении. Почему может возникнуть необходимость использовать один метод над другим?
-
Управление отделением слов и расширением глобов:
mapfile
не требует установки переменнойIFS
, упростив процесс управления входными данными. Это особенно полезно, когда строки содержат пробелы или специальные символы. -
Производительность и читаемость: В скриптинге, где требуется высокая производительность или сложные данные,
mapfile
может быть предпочтительнее, так как он работает с потоками данных напрямую, без лишних преобразований. -
Обход сбоев: В случае, если данные из команды содержат неожиданные символы и могут вызвать сбой при обработке через оператор
arr+=
, использованиеmapfile
будет более устойчивым.
Таким образом, при выборе между mapfile
и arr+=
необходимо учитывать природу ваших данных, специфику выполняемых операций и требования к обработке строки. Если вам важна точность и чистота данных, mapfile
предлагает более надежное решение. С другой стороны, простота и легкость написания arr+=
могут быть достаточно удобными в менее требовательных сценариях.
Таким образом, различия между этими двумя подходами связаны не только с синтаксисом, но и с фундаментальной разницей в том, как данные обрабатываются, что и формирует разнообразие их применения.