Розбийте рядок на масив у Bash


640

У сценарії Bash я хотів би розділити рядок на частини і зберегти їх у масиві.

Лінія:

Paris, France, Europe

Я хотів би мати їх у такому масиві:

array[0] = Paris
array[1] = France
array[2] = Europe

Я хотів би використовувати простий код, швидкість команди не має значення. Як я можу це зробити?


22
Це хіт Google №1, але у відповіді є суперечка, оскільки питання, на жаль, задає питання про розмежування , (пробіл у комах), а не про один символ , як кома. Якщо ви зацікавлені тільки в останньому випадку, відповіді тут легше слідувати: stackoverflow.com/questions/918886 / ...
antak

Якщо ви хочете з'єднати рядок і вам не байдуже, як він є масивом, cutце також має на увазі корисна команда bash. Сепаратор визначається en.wikibooks.org/wiki/Cut Ви також можете витягнути дані з структури запису фіксованої ширини. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Відповіді:


1088
IFS=', ' read -r -a array <<< "$string"

Зауважте, що символи в $IFSобробляються окремо як роздільники, так що в цьому випадку поля можуть бути розділені або комою, або пробілом, а не послідовністю двох символів. Цікаво, що порожні поля не створюються, коли на вході з’являється пробіл з комами, оскільки простір обробляється спеціально.

Для доступу до окремого елемента:

echo "${array[0]}"

Щоб повторити елементи:

for element in "${array[@]}"
do
    echo "$element"
done

Щоб отримати і індекс, і значення:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Останній приклад корисний, оскільки масиви Bash є рідкими. Іншими словами, ви можете видалити елемент або додати елемент, і тоді індекси не будуть суміжними.

unset "array[1]"
array[42]=Earth

Щоб отримати кількість елементів у масиві:

echo "${#array[@]}"

Як було сказано вище, масиви можуть бути рідкими, тому вам не слід використовувати довжину для отримання останнього елемента. Ось як можна в Bash 4.2 та пізніших версіях:

echo "${array[-1]}"

у будь-якій версії Bash (звідкись після 2.05b):

echo "${array[@]: -1:1}"

Більші від’ємні зсуви вибираються далі від кінця масиву. Відмітьте пробіл перед знаком мінус у старшій формі. Це потрібно.


15
Просто використовуйте IFS=', ', тоді вам не доведеться видаляти пробіли окремо. Тест:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: Дякую Я не знаю, що я думав. Мені declare -p array, до речі, подобається користуватися для тестових результатів.
Призупинено до подальшого повідомлення.

1
Це, здається, не поважає цитати. Наприклад, France, Europe, "Congo, The Democratic Republic of the"це розділиться після конго.
Ісраель Дов

2
@YisraelDov: Bash не може самостійно впоратися з CSV. Він не може визначити різницю між комами всередині лапок і тими, що знаходяться поза ними. Вам потрібно буде використовувати інструмент, який розуміє CSV, наприклад, lib мовою вищого рівня, наприклад модуль csv в Python.
Призупинено до подальшого повідомлення.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"розділиться на array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")ноту. Отже, це працює лише з полями без пробілів, оскільки IFS=', 'це набір окремих символів, а не роздільник рядків.
dawg

332

Усі відповіді на це питання так чи інакше неправильні.


Неправильна відповідь №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")

8
Також може бути корисно зауважити (хоча зрозуміло, що у вас не було місця для цього), що -dваріант readarrayспочатку з’являється в Bash 4.4.
fbicknel

2
Чудова відповідь (+1). Якщо ви поміняєте своє бажання awk '{ gsub(/,[ ]+|$/,"\0"); print }'і усунете це з'єднання фіналу, ", " вам не доведеться проходити гімнастику, щоб усунути фінальний запис. Отже: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")на Bash, який підтримує readarray. Зауважте, ваш метод - Bash 4.4+ і я вважаю, що через -dinreadarray
dawg

3
@datUser Це прикро. Ваша версія bash повинна бути занадто старою для readarray. У цьому випадку ви можете використовувати друге найкраще рішення, побудоване на основі read. Я маю на увазі це: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(із awkпідстановкою, якщо вам потрібна підтримка багаторозмірного роздільника). Повідомте мене, якщо у вас виникнуть якісь проблеми; Я впевнений, що це рішення повинно працювати на досить старих версіях bash, повертаючись до версії 2-щось, випущеної як два десятиліття тому.
bgoldst

1
Ух, яка геніальна відповідь! Хі-хі, моя відповідь: скинув баш сценарій і вистрілив пітон!
artfulrobot

1
@datUser баш на OSX досі застряг на рівні 3,2 (випущено близько 2007 р.); Я використав bash, знайдений у Homebrew, щоб отримати версії bash 4.X для OS X
JDS

222

Ось спосіб без встановлення IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

Ідея використовує заміну рядків:

${string//substring/replacement}

замінити всі збіги $ substring білим пробілом, а потім використовувати підстановлений рядок для ініціалізації масиву:

(element1 element2 ... elementN)

Примітка: у цій відповіді використовується оператор split + glob . Таким чином, для запобігання розширенню деяких символів (таких як *), корисно призупинити глобалізацію для цього сценарію.


1
Використовував такий підхід ... поки я не натрапив на довгу струну, щоб розколотися. 100% процесора більше хвилини (тоді я його вбив). Шкода, оскільки цей метод дозволяє розділити на рядок, а не на якийсь символ у IFS.
Вернер Леманн

100% час процесора протягом однієї хвилини звучить для мене так, ніби десь щось не так. Скільки часу тривав цей рядок, розмір МБ чи ГБ? Я думаю, як правило, якщо вам просто знадобиться невеликий роздільний рядок, ви хочете залишитися в Bash, але якщо це величезний файл, я би виконав щось подібне до Perl, щоб це зробити.

12
ПОПЕРЕДЖЕННЯ. Щойно зіткнувся з проблемою при такому підході. Якщо у вас є елемент з ім'ям *, ви отримаєте і всі елементи вашого cwd. таким чином string = "1: 2: 3: 4: *" дасть деякі несподівані та, можливо, небезпечні результати, залежно від вашої реалізації. Не отримали однакову помилку з (IFS = ',' read -a масив <<< "$ string"), і ця здається безпечною для використання.
Дітер Грібніц

4
цитування ${string//:/ }перешкоджає розширенню оболонки
Ендрю Уайт

1
Мені довелося використовувати наступне в OSX: array=(${string//:/ })
Марк Томсон,

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Друкується три


8
Я фактично віддаю перевагу такому підходу. Простий.
shrimpwagon

4
Я скопіював і вставив це, і це не працювало з відлунням, але працювало, коли я використовував його в циклі for.
Бен

2
Це не працює, як зазначено. @ Jmoney38 або shrimpwagon, якщо ви можете вставити це в термінал і отримати потрібний вихід, будь ласка, вставте результат тут.
abalter

2
@abalter працює для мене с a=($(echo $t | tr ',' "\n")). Той самий результат з a=($(echo $t | tr ',' ' ')).
листопад

@procrastinator Я просто спробував його VERSION="16.04.2 LTS (Xenial Xerus)"в bashраковині, і останній echoраз друкує порожню рядок. Яку версію Linux та яку оболонку ви використовуєте? На жаль, не можна відобразити термінальний сеанс у коментарі.
abalter

29

Іноді мені траплялося, що описаний у прийнятій відповіді спосіб не працював, особливо якщо роздільник - це повернення вагона.
У цих випадках я вирішив таким чином:

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"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 Це повністю працювало для мене. Мені потрібно було розмістити кілька рядків, розділених новою лінією, у масив, і я read -a arr <<< "$strings"не працював IFS=$'\n'.
Стефан ван ден Аккер


Це не зовсім відповідає початковому питанню.
Майк

29

Прийнята відповідь працює для значень в одному рядку.
Якщо змінна має кілька рядків:

string='first line
        second line
        third line'

Для отримання всіх рядків нам потрібна зовсім інша команда:

while read -r line; do lines+=("$line"); done <<<"$string"

Або набагато простіший читання bash :

readarray -t lines <<<"$string"

Друкувати всі лінії дуже просто, скориставшись функцією printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
Хоча не кожне рішення працює в кожній ситуації, ваша згадка про повторний масив ... замінила мої останні дві години на 5 хвилин ... ви отримали мій голос
Злий 84

7

Це схоже на підхід Jmoney38 , але з використанням sed:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

Друки 1


1
він друкує 1 2 3 4 в моєму випадку
minigeek

6

Ключовим фактором для розбиття рядка на масив є багатозначний роздільник ", ". Будь-яке рішення, що використовується IFSдля розмежувачів з декількома символами, по суті невірно, оскільки IFS - це набір цих символів, а не рядок.

Якщо ви призначите, IFS=", "рядок буде розриватися на ВСІ ","АБО " "або будь-яку їх комбінацію, що не є точним поданням роздільника двох символів ", ".

Ви можете використовувати awkабо sedрозділити рядок із заміною процесу:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Ефективніше використовувати регулярний вираз безпосередньо в Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

З другою формою немає підкошти, і вона буде притаманна швидше.


Редагувати bgoldst: Ось декілька орієнтирів, що порівнюють моє readarrayрішення з рішенням регулярного виразів Dawg, і я також включив readрішення для його виправлення (зверніть увагу: я трохи змінив рішення регексу для більшої гармонії з моїм рішенням (також дивіться мої коментарі нижче допис):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

Дуже круте рішення! Я ніколи не думав використовувати цикл для збігу регулярних виразів, чудового використання $BASH_REMATCH. Це працює, і справді не уникає нерестових підшарів. +1 від мене. Однак, під час критики, сам регулярний вираз є дещо неідеальним, оскільки, схоже, ви змушені були дублювати частину токена відмежувача (конкретно кома), щоб подолати відсутність підтримки не жадібних множників (також lookarounds) в ERE ("розширений" аромат регулярного викиду, вбудований в баш). Це робить його трохи менш загальним і надійним.
bgoldst

По-друге, я зробив деякий бенчмаркінг, і хоча продуктивність краща, ніж інші рішення для невеликих струн, вона погіршується експоненціально через багаторазову перебудову рядків, стаючи катастрофічною для дуже великих струн. Дивіться мою редакцію вашої відповіді.
bgoldst

@bgoldst: Який класний орієнтир! На захист регулярного вираження для 10-ти або 100-ти тисяч полів (те, що регекс розщеплюється), ймовірно, буде якась форма запису (як \nрозмежовані рядки тексту), що містить ці поля, тому катастрофічне уповільнення, швидше за все, не відбудеться. Якщо у вас є рядок зі 100 000 полями - можливо, Bash не є ідеальним ;-) Дякую за тест. Я дізнався річ чи дві.
dawg

4

Чисте багаторазове розмежувальне рішення.

Як зазначали інші у цій темі, питання ОП наводило приклад розбитої комою рядка на масив, але не вказувало, чи його цікавлять лише роздільники комами, роздільники символів або багато символів роздільники.

Оскільки Google прагне оцінити цю відповідь у верхній частині результатів пошуку чи біля неї, я хотів надати читачам чітку відповідь на питання про кілька розділових знаків, оскільки це також згадується принаймні в одній відповіді.

Якщо ви шукаєте рішення проблеми з розмежувачем з кількома символами, пропоную переглянути публікацію Mallikarjun M , зокрема відповідь від gniourf_gniourf, який забезпечує це елегантне чисте рішення BASH, використовуючи розширення параметрів:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Посилання на цитований коментар / посилання

Посилання на цитоване запитання: Як розділити рядок на багато символьний роздільник в bash?


1
Дивіться мій коментар щодо подібного, але вдосконаленого підходу.
xebeche

3

Це працює для мене на OSX:

string="1 2 3 4 5"
declare -a array=($string)

Якщо ваш рядок має різний роздільник, просто 1-ю замініть на пробіл:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Простий :-)


Працює і для Bash, і для Zsh, що є плюсом!
Ілля В.

2

Ще один спосіб зробити це без зміни IFS:

read -r -a myarray <<< "${string//, /$IFS}"

Замість того, щоб змінити IFS відповідно до потрібного роздільника, ми можемо замінити всі виникнення потрібного роздільника ", "вмістом $IFSvia "${string//, /$IFS}".

Може, це буде повільно для дуже великих струн?

Це ґрунтується на відповіді Денніса Вільямсона.


2

Я зіткнувся з цією публікацією, коли дивився, щоб проаналізувати введення типу: word1, word2, ...

ніщо з перерахованого вище мені не допомогло. вирішили це за допомогою awk. Якщо це комусь допомагає:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

Спробуйте це

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Це просто. Якщо ви хочете, ви також можете додати оголошення (а також видалити коми):

IFS=' ';declare -a array=(Paris France Europe)

IFS додано, щоб скасувати вищезазначене, але він працює без цього в новому екземплярі bash


1

Ми можемо використовувати команду tr, щоб розділити рядок на об’єкт масиву. Працює як MacOS, так і Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Інший варіант використання команди IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Використовуй це:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
Погано: залежно від розбиття слів та розширення назви шляху. Будь ласка, не відроджуйте старі питання з хорошими відповідями, щоб дати погані відповіді.
gniourf_gniourf

2
Це може бути поганою відповіддю, але це все-таки правильна відповідь. Флаггери / рецензенти: для невірних відповідей, таких як ця, downvote, не видаляйте!
Скотт Велдон

2
@gniourf_gniourf Не могли б ви пояснити, чому це погана відповідь? Я справді не розумію, коли це виходить з ладу.
Георгій Совєтов

3
@GeorgeSovetov: Як я вже сказав, це підлягає розщепленню слів і розширенню імені. У більш загальному сенсі , розщеплення рядки в масив , як array=( $string )це ( до жаль , дуже часто) антипаттерн: слово відбувається розщеплення: string='Prague, Czech Republic, Europe'; Розширення шляху відбувається: string='foo[abcd],bar[efgh]'не вдасться, якщо у вашому каталозі є ім’я, наприклад, foodабо Єдине дійсне використання такої конструкції, коли це глобус. barfstring
gniourf_gniourf

0

ОНОВЛЕННЯ: Не робіть цього через проблеми з eval.

З трохи меншою церемонією:

IFS=', ' eval 'array=($string)'

напр

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
eval - це зло! не роби цього.
цезарсол

1
Pfft. Ні. Якщо ви пишете сценарії, досить великі, щоб це мало значення, ви робите це неправильно. У коді програми, eval - це зло. У сценаріях оболонок це звичайне, необхідне та невпливне значення.
користувач1009908

2
помістіть $у свою змінну, і ви побачите ... Я пишу багато сценаріїв, і мені ніколи не доводилося використовувати одинeval
кесарсол

2
Ви маєте рацію, це корисно лише тоді, коли відомо, що вхід чистий. Не надійне рішення.
користувач1009908

Єдиний раз, коли мені доводилося використовувати eval, це програма, яка б сама генерувала власний код / ​​модулі ... І це ніколи не було жодної форми введення користувача ...
Злий 84

0

Ось мій хак!

Розбиття рядків на рядки - це досить нудна справа, використовуючи bash. Що трапляється, це те, що у нас обмежені підходи, які працюють лише в кількох випадках (розділені на ";", "/", "." Тощо) або у нас є різні побічні ефекти у результатах.

Підхід нижче вимагав низки маневрів, але я вірю, що він спрацює для більшості наших потреб!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

Щодо мультильованих елементів, чому б не щось подібне

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Іншим способом було б:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Тепер ваші елементи зберігаються в масиві "arr". Щоб повторити елементи:

for i in ${arr[@]}; do echo $i; done

1
Я висвітлюю цю ідею у своїй відповіді ; див. Неправильна відповідь №5 (можливо, вас особливо зацікавить моє обговорення evalтрюку). Ваше рішення залишає $IFSвстановлене значення пробілу комами після факту.
bgoldst

-1

Оскільки існує так багато способів вирішити це питання, почнемо з визначення того, що ми хочемо бачити в нашому рішенні.

  1. Bash забезпечує вбудований readarrayдля цієї мети. Давайте скористаємося цим.
  2. Уникайте негарних і непотрібних хитрощів, таких як зміна IFS, циклічне використання eval, використання або додавання зайвого елемента, а потім видалення.
  3. Знайдіть простий, читабельний підхід, який легко можна адаптувати до подібних проблем.

readarrayКоманда проста у використанні з новим рядком в якості роздільника. Інші роздільники можуть додавати додатковий елемент до масиву. Найчистіший підхід полягає в тому, щоб спочатку адаптувати свій внесок у форму, з якою добре працює, readarrayперш ніж передати його.

Вхід у цьому прикладі не має багатохарактерного роздільника. Якщо ми застосуємо трохи здорового глузду, це найкраще розуміти як введення, розділене комами, для якого, можливо, потрібно буде обрізати кожен елемент. Моє рішення - розділити введення комою на кілька рядків, обрізати кожен елемент і передати все це readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Іншим підходом може бути:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Після цього 'arr' - це масив з чотирма рядками. Для цього не потрібно мати справу з IFS, читанням чи будь-якими іншими спеціальними матеріалами, отже, набагато простішими та прямими.


Той самий (на жаль поширений) антипатерн, як і інші відповіді: за умови розщеплення слів та розширення назви файлів.
gniourf_gniourf
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.