Как разбить строку с разделителями табуляции в сценарии bash БЕЗ схлопывания пробелов?

У меня есть строка в $LINE, и я хочу, чтобы $ITEMS была ее массивной версией, разделенной на отдельные вкладки и сохраняющей пробелы. Вот где я сейчас:

IFS=$'\n' ITEMS=($(echo "$LINE" | tr "\t" "\n"))

Проблема здесь в том, что IFS - это один или несколько, поэтому он поглощает новые строки, вкладки и все такое. Я пробовал несколько других вещей, основанных на других вопросах, опубликованных здесь, но они предполагают, что во всех полях всегда будет значение, а не пустое. И тот, который, кажется, содержит ключ это далеко от меня и работает со всем файлом (я просто разбиваю одну строку).

Я предпочитаю решение на чистом BASH.


person Neil C. Obremski    schedule 01.11.2013    source источник


Ответы (5)


IFS - это только один или несколько, если символы являются пробелами. Символы, не являющиеся пробелами, являются одиночными разделителями. Таким образом, простое решение, если есть какой-то непробельный символ, которого, как вы уверены, нет в вашей строке, состоит в том, чтобы перевести табуляцию на этот символ, а затем разделить его:

IFS=$'\2' read -ra ITEMS <<<"${LINE//$'\t'/$'\2'}"

К сожалению, такие предположения, как «во входных данных нет экземпляра \2», как правило, терпят неудачу в долгосрочной перспективе, где «в долгосрочной перспективе» переводится как «в самый неподходящий момент». Таким образом, вы можете сделать это в два этапа:

IFS=$'\2' read -ra TEMP < <(tr $'\t\2' $'\2\t' <<<"$LINE")
ITEMS=("${TEMP[@]//$'\t'/$'\2'}")
person rici    schedule 01.11.2013

Одна из возможностей: вместо разделения с помощью IFS используйте параметр -d для read «строк», заканчивающихся табуляцией, из строки. Однако вам нужно убедиться, что ваша строка заканчивается также табуляцией, иначе вы потеряете последний элемент.

items=()
while IFS='' read -r -d$'\t' x; do
   items+=( "$x" )
done <<< $'   foo   \t  bar\nbaz \t   foobar\t'

printf "===%s===\n" "${items[@]}"

Обеспечение конечной вкладки без добавления дополнительного поля может быть выполнено с помощью

if [[ $str != *$'\t' ]]; then str+=$'\t'; fi

если необходимо.

person chepner    schedule 01.11.2013
comment
Забавно, я видел -d и безуспешно пытался сам что-то из него сделать; Я вижу, что ключ использует цикл (я пробовал комбинировать с -a). Один вопрос: зачем заранее ставить IFS=''? - person Neil C. Obremski; 02.11.2013
comment
Это необходимо, если одна из строк с разделителями табуляции начинается или заканчивается пробелом, так как read удалит это перед установкой значения x со значением по умолчанию IFS. - person chepner; 02.11.2013
comment
Чтобы справиться с отсутствующим переводом строки в конце, вы можете заменить оператор read в тесте while на IFS='' read -r -d$'\t' x || [[ $x ]] или просто добавить items+=( "$x" ) после цикла while. - person gniourf_gniourf; 30.10.2014
comment
items+=("$x") после того, как цикл добавит пустую строку, если в файле не отсутствует последняя новая строка, поэтому вам понадобится защита, такая как (( $? )) && items+=("$x"). (Не проверено, и есть сложные угловые случаи, поэтому я не уверен, что это на 100% правильно.) - person chepner; 30.10.2014

Специальные символы IFS:

Words of the form $'string' are treated specially.  The word expands to
string, with backslash-escaped characters replaced as specified by  the
ANSI  C  standard.  Backslash escape sequences, if present, are decoded
as follows:
       \a     alert (bell)
       \b     backspace
       \e
       \E     an escape character
       \f     form feed
       \n     new line
       \r     carriage return
       \t     horizontal tab
       \v     vertical tab
       \\     backslash
       \'     single quote
       \"     double quote
       \?     question mark
       \nnn   the eight-bit character whose value is  the  octal  value
              nnn (one to three digits)
       \xHH   the  eight-bit  character  whose value is the hexadecimal
              value HH (one or two hex digits)
       \uHHHH the Unicode (ISO/IEC 10646) character whose value is  the
              hexadecimal value HHHH (one to four hex digits)
       \UHHHHHHHH
              the  Unicode (ISO/IEC 10646) character whose value is the
              hexadecimal value HHHHHHHH (one to eight hex digits)
       \cx    a control-x character 

Расширенный результат заключен в одинарные кавычки, как если бы знака доллара не было.

Строка в двойных кавычках, перед которой стоит знак доллара ($"string"), приведет к переводу строки в соответствии с текущей локалью. Если текущая локаль C или POSIX, знак доллара игнорируется. Если строка переводится и заменяется, замена заключается в двойные кавычки.

person Nathan SR    schedule 30.06.2018

Чистое решение bash, которое будет разделяться только на вкладки и сохранять новые строки и другие забавные символы, если они есть:

IFS=$'\t' read -r -a arr -d '' < <(printf '%s' "$line")

Попытайся:

$ line=$'zero\tone with\nnewlines\ttwo\t     three   \n\t\tfive\n'
$ IFS=$'\t' read -r -a arr -d '' < <(printf '%s' "$line")
$ declare -p arr
declare -a arr='([0]="zero" [1]="one with
newlines" [2]="two" [3]="     three   
" [4]="five
")'

Как видите, работает безотказно: сохраняет все (пробелы, новые строки и т.д.), разбивает только по символам табуляции.

Есть один недостаток: он не обрабатывает «пустые поля»: обратите внимание, что в line есть две последовательные вкладки; мы ожидали бы получить пустое поле в arr, но это не так.

Есть еще один менее очевидный недостаток: код возврата read равен 1, так что технически для Bash эта команда не работает. Это абсолютно не проблема, если только вы не используете set -e или set -E, но в любом случае это не рекомендуется (поэтому и не следует).

Если вы можете жить с этими двумя небольшими недостатками, это может быть идеальным решением.

Обратите внимание, что мы используем < <(printf '%s' "$line"), а не <<< "$line" для передачи read, так как последний вставляет завершающую новую строку.

person gniourf_gniourf    schedule 30.10.2014

line=$'zero\tone\ttwo'
IFS=$'\t' read -a arr <<< "${line}"
declare -p

Выход

declare -a arr='([0]="zero" [1]="one" [2]="two")'

Примечание. Это не касается новых строк в line.

person hrushikesh    schedule 30.10.2014