Усі відповіді на це питання так чи інакше неправильні.
Неправильна відповідь №1
IFS=', ' read -r -a array <<< "$string"
1: Це неправильне використання $IFS
. Значення $IFS
змінного НЕ приймаються в якості однієї змінною довжиною рядка сепаратора, а вона береться в якості набору з односимвольних строкових сепараторів, де кожне поле , яке read
відщеплюється від вхідних лінії може бути припинено з допомогою будь-якого символу в наборі (кома або пробіл, у цьому прикладі).
Насправді, для справжніх наклейок там повне значення $IFS
має дещо більше. З посібника з bash :
Оболонка розглядає кожен символ IFS як роздільник, і розбиває результати інших розширень на слова, використовуючи ці символи як термінатори поля. Якщо IFS не встановлено або його значення точно <space><tab> <newline> , за замовчуванням, то послідовності <space> , <tab> і <newline> на початку та в кінці результатів попередніх розширень. ігноруються, і будь-яка послідовність символів IFS не на початку чи в кінці служить для розмежування слів. Якщо IFS має значення, відмінне від типового, то послідовності символів пробілу <пробіл> , <та> та <ігноруються на початку та в кінці слова, якщо символ пробілу знаходиться у значенні IFS ( символ пробілу IFS ). Будь-який символ у IFS, який не є пробілом IFS , поряд із будь-якими суміжними символами пробілу IFS , обмежує поле. Послідовність символів пробілу IFS також розглядається як роздільник. Якщо значення IFS є нульовим, розщеплення слів не відбувається.
В основному, для ненульових значень $IFS
, що не мають значення за замовчуванням , поля можна розділити з будь-яким (1) послідовністю одного або декількох символів, що є всіма з набору символів пробілів IFS (тобто залежно від <space> , <tab> і <newline> ("новий рядок", що означає канал рядка (LF) ) присутні будь-де в $IFS
), або (2) будь-який не "символ пробілу IFS", який присутній $IFS
разом із будь-якими "символами пробілів IFS" навколо нього у рядку введення.
Для ОП можливо, що другий режим розділення, який я описав у попередньому абзаці, є саме тим, що він хоче для своєї вхідної рядки, але ми можемо бути впевнені, що перший описаний мною режим розділення зовсім невірний. Наприклад, що робити, якщо його вхідний рядок був 'Los Angeles, United States, North America'
?
IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")
2: Навіть якщо ви використовували це рішення за допомогою розділового символу (наприклад, кома сама по собі, тобто без наступного місця або іншого багажу), якщо значення $string
змінної містить будь-які НР, то read
буде припиніть обробку, як тільки вона зустрінеться з першим НЧ. read
Вбудований обробляє тільки один рядок на виклик. Це справедливо навіть у тому випадку, якщо ви конфігуруєте або перенаправляєте вхід лише до read
оператора, як це робимо в цьому прикладі з механізмом тут-рядка , і, таким чином, необроблений вхід гарантовано буде втрачений. Код, що використовує read
вбудований, не знає потоку даних в його структурі команд.
Ви можете стверджувати, що це навряд чи спричинить проблеми, але все-таки це тонка небезпека, якої слід уникати, якщо це можливо. Це викликано тим, що read
вбудований насправді робить два рівні розбиття входу: спочатку на лінії, потім на поля. Оскільки ОП хоче лише одного рівня розщеплення, це використання read
вбудованого не є доцільним, і нам слід цього уникати.
3: Неочевидною потенційною проблемою з цим рішенням є те, що read
завжди випадає поле остачі, якщо воно порожнє, хоча воно інакше зберігає порожні поля. Ось демонстрація:
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")
Можливо, ОП не переймається цим питанням, але це все-таки обмеження, про яке варто знати. Це знижує надійність і загальність рішення.
Цю проблему можна вирішити, додавши фіксуючий обмежувач до вхідного рядка безпосередньо перед його подачею read
, як я продемонструю пізніше.
Неправильна відповідь №2
string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
Подібна ідея:
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(Примітка. Я додав відсутні дужки навколо підстановки команди, яку, здається, відповідач опустив.)
Подібна ідея:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
Ці рішення використовують розділення слів у призначенні масиву, щоб розділити рядок на поля. Як не дивно, як read
і загальне розщеплення слів, також використовується $IFS
спеціальна змінна, хоча в цьому випадку мається на увазі, що вона встановлена за замовчуванням <space><tab> <newline> , а тому будь-яка послідовність одного або декількох IFS символи (які зараз усі символи пробілу) вважаються роздільником поля.
Це вирішує проблему двох рівнів розщеплення, здійснених шляхом read
, оскільки розділення слів само по собі становить лише один рівень розщеплення. Але так само, як і раніше, проблема полягає в тому, що окремі поля в ряді вводу вже можуть містити $IFS
символи, і таким чином вони будуть неправильно розбиватися під час операції розбиття слів. Це трапляється не так для жодного зразків вхідних рядків, наданих цими відповідями (як зручно ...), але, звичайно, це не змінює факту, що будь-яка база коду, яка використовувала цю ідіому, тоді ризикувала б підірвати, якщо це припущення колись було порушено в якийсь момент вниз по лінії. Ще раз розгляньте мій контрприклад 'Los Angeles, United States, North America'
(або 'Los Angeles:United States:North America'
).
Крім того , слово розщеплення зазвичай слід розширення імені файлу ( ака імен файлів ака підстановки), який, якщо зроблена, потенційно корумповані слова , що містять символи *
, ?
або [
слід ]
(і, якщо extglob
встановлений, Дужки фрагменти передують ?
, *
, +
, @
, або !
) шляхом їх зіставлення з об'єктами файлової системи та відповідним чином розширенням слів ("глобусів"). Перший із цих трьох відповідачів спритно подолав цю проблему, запустивши set -f
попередньо, щоб відключити глобалізацію. Технічно це працює (хоча, мабуть, варто додатиset +f
згодом до повторного ввімкнення глобалізації для наступного коду, який може залежати від нього), але небажано мати возитися з глобальними налаштуваннями оболонки, щоб зламати основні операції розбору рядка до масиву в локальному коді.
Ще одне питання з цією відповіддю полягає в тому, що всі порожні поля будуть втрачені. Це може бути або не бути проблемою, залежно від програми.
Примітка. Якщо ви збираєтеся використовувати це рішення, краще використовувати ${string//:/ }
форму "заміни шаблону" розширення параметрів , а не виникати проблеми виникнення підстановки команди (яка розщеплює оболонку), запуску конвеєра та запуск зовнішнього виконуваного файлу ( tr
або sed
), оскільки розширення параметра - це суто внутрішня операція оболонки. (Також для tr
і sed
рішень вхідна змінна повинна бути подвійною цитатою всередині підстановки команди; інакше розщеплення слів почне діяти в echo
команді і, можливо, зіпсується зі значеннями поля. Також $(...)
форма заміни команд є кращою для старої`...`
форма, оскільки вона спрощує вкладення підстановок команд і дозволяє краще виділити синтаксис текстовими редакторами.)
Неправильна відповідь №3
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
Ця відповідь майже така сама, як №2 . Різниця полягає в тому, що відповідач зробив припущення, що поля розмежовані двома символами, один з яких представлений за замовчуванням $IFS
, а інший - ні. Він вирішив цей досить специфічний випадок, видаливши символи, не представлені IFS, за допомогою розширення підстановки шаблону, а потім за допомогою розділення слів для розділення полів на залишився символом роздільника, представленого IFS.
Це не дуже загальне рішення. Крім того, можна стверджувати, що кома насправді є "первинним" символом розмежувача, і те, що знімати її та залежно від символу простору для розбиття поля, просто неправильно. Ще раз розглянемо мої контрприклад: 'Los Angeles, United States, North America'
.
Також, знову ж таки, розширення імені файлів може пошкодити розгорнуті слова, але це можна запобігти, тимчасово відключивши глобус для призначення з, set -f
а потім set +f
.
Також знову будуть втрачені всі порожні поля, що може залежати від програми, а може і не бути проблемою.
Неправильна відповідь №4
string='first line
second line
third line'
oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"
Це схоже на №2 та №3 тим, що він використовує розбиття слів для виконання завдання, лише тепер код явно встановлює, що $IFS
він містить лише символьний роздільник поля, присутній у рядку введення. Слід повторити, що це не може працювати для багаторозрядних роздільників поля, таких як роздільник місця з комою та пробіл OP. Але для розділового знака з одним символом, як LF, використаного в цьому прикладі, він насправді наближається до досконалості. Поля не можна ненавмисно розбивати посередині, як ми бачили з попередніми неправильними відповідями, і є лише один рівень розщеплення, як потрібно.
Одна з проблем полягає в тому, що розширення імені файлів може пошкодити зачіпані слова, як описано раніше, хоча це ще раз можна вирішити, перевівши критичний вислів у set -f
та set +f
.
Ще одна потенційна проблема полягає в тому, що оскільки LF кваліфікується як "символ пробілу IFS", як визначено раніше, всі порожні поля будуть втрачені, як і в №2 та №3 . Це, звичайно, не буде проблемою, якщо роздільник буде не символом «пробілу IFS», і залежно від програми це все одно не має значення, але це погіршує загальність рішення.
Отже, підводячи підсумок, якщо припустити, що ви маєте односимвольний роздільник, і це або не "символ пробілу IFS", або вам не байдуже порожні поля, і ви загортаєте критичне твердження в set -f
і set +f
, тоді це рішення працює , але інакше ні.
(Крім того, для інформації, присвоєння LF змінної в bash можна простіше зробити з $'...'
синтаксисом, наприклад IFS=$'\n';
.)
Неправильна відповідь №5
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
Подібна ідея:
IFS=', ' eval 'array=($string)'
Це рішення є фактично перехрестом між №1 (тим, що він встановлює $IFS
пробіл комами) та №2-24 (оскільки він використовує розділення слів для розділення рядка на поля). Через це вона страждає від більшості проблем, які стикаються з усіма вищезазначеними помилковими відповідями, схожими на найгірші з усіх світів.
Крім того, щодо другого варіанту може здатися, що eval
виклик є абсолютно непотрібним, оскільки його аргумент є одноцитованим рядковим літералом, а тому є статистично відомим. Але насправді дуже корисна для використання eval
таким чином. Зазвичай, коли ви запускаєте команду простий , який складається з присвоєння змінної тільки , тобто без фактичного командного слова після нього, призначення вступає в силу в середовищі оболонки:
IFS=', '; ## changes $IFS in the shell environment
Це справедливо навіть у тому випадку, якщо проста команда включає декілька змінних призначень; знову ж таки, доки немає командного слова, всі призначення змінних впливають на середовище оболонки:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
Але, якщо присвоєння змінної приєднано до імені команди (я люблю це називати "присвоюванням префікса"), це не впливає на середовище оболонки, а натомість впливає лише на середовище виконаної команди, незалежно від того, вбудована вона або зовнішній:
IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it
Відповідна цитата з посібника з bash :
Якщо ніяких імен команд немає, зміни змінних впливають на поточне середовище оболонки. В іншому випадку змінні додаються до середовища виконаної команди і не впливають на поточне середовище оболонки.
Можна використовувати цю функцію присвоєння змінної $IFS
лише тимчасово, що дозволяє уникнути цілого гамбіту збереження та відновлення, такого як це робиться зі $OIFS
змінною у першому варіанті. Але проблема, з якою ми стикаємося тут, полягає в тому, що команда, яку нам потрібно виконати, сама по собі є простою змінною задачею, і, отже, вона не буде залучати командне слово, щоб зробити $IFS
завдання тимчасовим. Ви можете подумати собі, ну чому б просто не додати команду слово no-op до заяви, як, наприклад, : builtin
зробити $IFS
завдання тимчасовим? Це не працює, тому що потім це $array
завдання також буде тимчасовим:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
Таким чином, ми ефективно знаходимось у глухому куточку. Але, коли він eval
запускає свій код, він запускає його в середовищі оболонки, як би це було нормальним, статичним вихідним кодом, і тому ми можемо виконати $array
призначення всередині eval
аргументу, щоб воно набуло чинності в середовищі оболонки, тоді як $IFS
призначення префікса, що префікс eval
команди не переживе eval
команду. Саме ця хитрість використовується у другому варіанті цього рішення:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does
Отже, як бачите, це насправді досить розумний трюк, і виконує саме те, що потрібно (принаймні, щодо виконання завдання) досить неочевидним чином. Я насправді не проти цієї хитрості взагалі, незважаючи на участь eval
; просто будьте обережні, щоб однозначно цитувати рядок аргументів, щоб захистити від загроз безпеці.
Але знову ж таки, через агломерацію проблем "найгіршого з усіх світів", це все-таки неправильна відповідь на вимогу ОП.
Неправильна відповідь №6
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
Гм ... що? OP має рядкову змінну, яку потрібно розібрати в масив. Ця "відповідь" починається з дослівного вмісту вхідного рядка, вставленого в буквальний масив. Я думаю, що це один із способів зробити це.
Схоже, відповідач, можливо, припускав, що $IFS
змінна впливає на весь bash синтаксичний аналіз у всіх контекстах, що не відповідає дійсності. З посібника з bash:
IFS Внутрішній роздільник поля, який використовується для розбиття слів після розширення та для розділення рядків на слова з прочитаною вбудованою командою. Значенням за замовчуванням є <space><tab> <newline> .
Отже, $IFS
спеціальна змінна фактично використовується лише у двох контекстах: (1) розбиття слів, яке виконується після розширення (мається на увазі не під час розбору вихідного коду bash) та (2) для розділення вхідних рядків на слова read
вбудованим.
Дозвольте спробувати зробити це зрозумілішим. Я думаю, що може бути добре провести межу між розбором та виконанням . Bash спочатку повинен проаналізувати вихідний код, який, очевидно, є синтаксичним розбором , а потім пізніше він виконує код, який є, коли розширення надходить у зображення. Розширення дійсно подія виконання . Крім того, я приймаю питання з описом $IFS
змінної, яку я тільки цитував вище; замість того, щоб сказати, що розщеплення слів виконується після розширення , я б сказав, що розщеплення слів виконується під час розширення, або, можливо, навіть точніше, розбиття слів є частиноюпроцес розширення. Словосполучення "розщеплення слів" стосується лише цього кроку розширення; його ніколи не слід використовувати для позначення розбору вихідного коду bash, хоча, на жаль, документи, здається, багато підкидають слова "розділити" та "слова". Ось релевантний уривок з версії linux.die.net керівництва bash:
Розширення виконується в командному рядку після його поділу на слова. Є сім видів розширення виконується: в фігурних дужках , тильди , параметрів і змінних розширення , підстановки команд , арифметичне розширення , слово розщеплення і розширення імен файлів .
Порядок розширень: розширення дужок; розширення тильди, розширення параметрів і змінних, арифметичне розширення та заміна команд (виконується зліва направо); розділення слів; і розширення імені шляху.
Ви можете стверджувати, що версія посібника GNU робить дещо краще, оскільки в першому реченні розділу розширення вибирає слово "жетони" замість "слова":
Розширення виконується в командному рядку після його розбиття на маркери.
Важливим моментом є те, $IFS
що не змінюється спосіб bash аналізує вихідний код. Розбір вихідного коду bash - це насправді дуже складний процес, який включає розпізнавання різних елементів граматики оболонки, таких як командні послідовності, списки команд, конвеєри, розширення параметрів, арифметичні підстановки та підстановки команд. Здебільшого процес розбору bash не може бути змінений діями на рівні користувача, як-от присвоєння змінних (насправді, з цього правила є деякі незначні винятки; наприклад, перегляньте різні compatxx
настройки оболонки, що може змінити певні аспекти поведінки синтаксичного аналізу. Потім "слова" / "лексеми", що виникають в результаті цього складного процесу розбору, потім розширюються відповідно до загального процесу "розширення", як розбито на вищезазначені витяги документації, де розбиття тексту на розгорнутий (розширюється?) Текст на низхідний потік слова - це просто один крок цього процесу. Розбиття слова стосується лише тексту, який був викинутий з попереднього кроку розширення; це не впливає на буквальний текст, який був розібраний прямо біля вихідного потоку.
Неправильна відповідь №7
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
Це одне з найкращих рішень. Зауважте, що ми знову користуємося read
. Хіба я не говорив раніше, що read
це недоречно, оскільки воно виконує два рівні розщеплення, коли нам потрібен лише один? Хитрість тут полягає в тому, що ви можете зателефонувати read
таким чином, що він фактично виконує лише один рівень розщеплення, зокрема, розділяючи лише одне поле за викликом, що обумовлює необхідність витрат на повторний виклик у циклі. Це трохи хитра рука, але це працює.
Але є проблеми. По-перше: Коли ви надаєте принаймні один аргумент NAMEread
, він автоматично ігнорує провідні та кінцеві пробіли у кожному полі, яке відокремлено від рядка введення. Це відбувається незалежно від того $IFS
, встановлено значення за замовчуванням чи ні, як описано раніше в цій публікації. Тепер ОП може не перейматися цим для свого конкретного випадку використання, і насправді це може бути бажаною особливістю поведінки розбору. Але не кожен, хто хоче розібрати рядок на поля, захоче цього. Однак є рішення: Дещо неочевидне використання read
- передавати нульові аргументи NAME . У цьому випадку read
буде збережено весь рядок введення, який він отримує з вхідного потоку, у змінній з назвою $REPLY
, і, як бонус, він не будесмуга, що веде і відстає пробіл від значення. Це дуже надійне використання, read
яке я часто використовував у своїй кар'єрі програмування оболонок. Ось демонстрація різниці в поведінці:
string=$' a b \n c d \n e f '; ## input string
a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a b" [1]="c d" [2]="e f") ## read trimmed surrounding whitespace
a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]=" a b " [1]=" c d " [2]=" e f ") ## no trimming
Друга проблема цього рішення полягає в тому, що він насправді не стосується випадку спеціального роздільника поля, такого як кома-простір OP. Як і раніше, багатоканальні роздільники не підтримуються, що є прикрою обмеженням цього рішення. Ми можемо спробувати хоча б розділити кому, вказавши роздільник для -d
параметра, але подивіться, що відбувається:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")
Передбачувано, що неврахуваний навколишній пробіл потрапляє до значень поля, а значить, це доведеться згодом виправити за допомогою операцій обрізки (це також можна зробити безпосередньо в циклі while). Але є ще одна очевидна помилка: Європа відсутня! Що з ним сталося? Відповідь полягає в тому, що read
повертає невдалий код повернення, якщо він потрапляє в кінець файлу (у цьому випадку ми можемо назвати його кінцевим рядком), не зустрічаючи кінцевого польового термінатора в остаточному полі. Це призводить до того, що цикл while передчасно розривається, і ми втрачаємо остаточне поле.
Технічно ця сама помилка вплинула і на попередні приклади; різниця полягає в тому, що роздільник поля прийнято вважати LF, що є типовим типом, коли ви не вказуєте -d
параметр, і механізм <<<
("тут-рядок") автоматично додає LF до рядка безпосередньо перед тим, як він подає його як вхід до команди. Отже, у таких випадках ми неначе випадково вирішили проблему випавшого кінцевого поля, мимоволі додавши до входу додатковий фіктивний термінатор. Назвемо це рішення рішенням "фіктивного термінатора". Ми можемо застосувати рішення фіктивного термінатора вручну для будь-якого спеціального роздільника, з'єднавши його з вхідним рядком самостійно, коли інстанціювати його в рядок here:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Там проблема вирішена. Іншим рішенням є лише розірвати цикл while, якщо обидва (1) read
повернулися відмовою і (2) $REPLY
порожніми, тобто read
не вдалося прочитати жодних символів до потрапляння в кінець файлу. Демонстрація:
a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Цей підхід також розкриває таємний LF, який автоматично додається до рядка тут <<<
оператором перенаправлення. Звичайно, це можна було б зняти окремо за допомогою чіткої операції обрізки, як описано мить тому, але очевидно, що підхід до манекен-термінатора вирішує це безпосередньо, тому ми могли просто піти з цим. Рішення вручну-фіктивного термінатора насправді досить зручне тим, що воно вирішує обидві ці дві задачі (проблема викинутого кінцевого поля та додана проблема НЧ) за один раз.
Отже, загалом це досить потужне рішення. Єдиною слабкою стороною є відсутність підтримки для багатохарактерних роздільників, про які я звернуся пізніше.
Неправильна відповідь №8
string='first line
second line
third line'
readarray -t lines <<<"$string"
(Це фактично з тієї ж посади, що і № 7 ; відповідач запропонував два рішення в тому ж самому дописі.)
readarray
Вбудований, який є синонімом mapfile
, є ідеальним. Це вбудована команда, яка розбирає bytestream в змінну масиву за один кадр; не возитися з петлями, умовними умовами, замінами чи чим-небудь іншим. І це не видаляє скрипок жодного пробілу з вхідного рядка. І (якщо -O
це не вказано), він зручно очищає цільовий масив перед тим, як призначити його. Але це все ще не досконало, отже, моя критика як "неправильна відповідь".
По-перше, просто щоб це не вийти з ладу, зауважте, що так само, як і поведінка read
під час аналізу поля, readarray
випадає поле сліду, якщо воно порожнє. Знову ж таки, це, мабуть, не стосується ОП, але це може бути для деяких випадків використання. Я повернусь до цього через мить.
По-друге, як і раніше, він не підтримує багатохарактерні роздільники. Я дам виправлення цьому також через мить.
По-третє, написане рішення не розбирає вхідний рядок ОП, і насправді воно не може бути використане як-це для його розбору. Я на це миттєво розширюся.
З вищезазначених причин я все ще вважаю це "неправильною відповіддю" на питання ОП. Нижче я дам те, що вважаю правильною відповіддю.
Правильна відповідь
Ось наївна спроба змусити номер 8 працювати, просто вказавши -d
варіант:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Ми бачимо, що результат ідентичний тому, який ми отримали при подвійному умовному підході до циклічного read
рішення, обговореному в №7 . Ми майже можемо вирішити це за допомогою трюку вручну: фіктивний термінатор:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
Проблема тут полягає в тому, що readarray
збережене трейлінг-поле, оскільки <<<
оператор перенаправлення додав LF до вхідного рядка, а значить, трейлінг-поле не було порожнім (інакше воно було б випало). Ми можемо подбати про це шляхом явного скидання остаточного елемента масиву після факту:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Єдині дві проблеми, які залишаються, які насправді пов’язані, - це (1) стороннє пробіл, яке потрібно обрізати, і (2) відсутність підтримки для розмежувачів багатохарактерних знаків.
Пробіл білого кольору, звичайно, може бути оброблений згодом (наприклад, див. Як обрізати пробіл із змінної Bash? ). Але якщо ми можемо зламати багатохарактерний роздільник, то це дозволить вирішити обидві проблеми за один кадр.
На жаль, немає прямого способу змусити мультихарактерний роздільник до роботи. Найкраще рішення, про яке я думав, - це попередня обробка вхідного рядка для заміни мультихарактерного роздільника на один символьний роздільник, який гарантовано не зіткнеться зі вмістом вхідного рядка. Єдиний символ, який має цю гарантію, - байт NUL . Це тому, що в bash (хоч і не в zsh, до речі) змінні не можуть містити байт NUL. Цей етап попередньої обробки може бути виконаний вбудованим шляхом заміни процесу. Ось як це зробити за допомогою awk :
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
Там, нарешті! Це рішення не помилково розділить поля посередині, не буде вирізано передчасно, не випаде порожніми полями, не зіпсується на розширеннях імен файлів, не автоматично зніме провідні та кінцеві пробіли, не залишить спокійного місця в кінці, не вимагає циклів і не погоджується з однозначним роздільником.
Розчин для обрізки
Нарешті, я хотів продемонструвати власне досить складне рішення для обрізки, використовуючи неясний -C callback
варіант readarray
. На жаль, у мене не вистачило місця проти драконівського обмеження розміром 30 000 символів Stack Overflow, тому я не зможу пояснити це. Я залишу це як вправу для читача.
function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
,
(пробіл у комах), а не про один символ , як кома. Якщо ви зацікавлені тільки в останньому випадку, відповіді тут легше слідувати: stackoverflow.com/questions/918886 / ...