Поиск специальных символов с помощью grep

Описание

Понимание этого потребует некоторых усилий. Будьте терпеливы. Решение будет корректно работать в bash. Необходимы некоторые "башисты".

Первое: Нам нужно использовать "Косвенный" доступ к переменной ${!variable}. Если $variable содержит строку animal_name, то "Расширение параметра": ${!variable} расширится до содержимого $animal_name.

Давайте посмотрим эту идею в действии, я сохранил имена и значения, которые вы использовали, где это возможно, чтобы вам было легче понять:

#!/bin/bash

function delim_to_array() {
    local VarName=$1

    local IFS="$2";
    printf "inside  IFS=<%s>\n" "$IFS"

    echo "inside  var    $VarName"
    echo "inside  list = ${!VarName}"

    echo a\=\(${!VarName}\)
    eval a\=\(${!VarName}\)
    printf "in  <%s> " "${a[@]}"; echo

    eval $VarName\=\(${!VarName}\)
}

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","

printf "out <%s> " "${animal_list[@]}"; echo
printf "outside IFS=<%s>\n" "$IFS"

# Now we can use animal_name as an array
for animal in "${animal_list[@]}"; do
    echo "NAME: $animal"
done

Если этот полный скрипт будет выполнен (предположим, что он называется so-setvar.sh), вы должны увидеть:

$ ./so-setvar.sh
inside  IFS=<,>
inside  var    animal_list
inside  list = anaconda, bison, cougar, dingo
a=(anaconda  bison  cougar  dingo)
in   in   in   in   
out  out  out  out  
outside IFS=< 
>
NAME: anaconda
NAME: bison
NAME: cougar
NAME: dingo

Поймите, что "внутри" означает "внутри функции", а "снаружи" - наоборот.

Значение внутри $VarName - это имя переменной: animal_list, как строка.

Значение в ${!VarName} - это список: anaconda, bison, cougar, dingo

Теперь, чтобы показать, как построено решение, есть строка с echo:

echo a\=\(${!VarName}\)

которая показывает, что выполняет следующая строка с eval:

a=(anaconda  bison  cougar  dingo)

После того, как evaluated, переменная a представляет собой массив со списком животных. В данном случае переменная a используется для того, чтобы показать, как именно eval влияет на нее.

А затем значения каждого элемента a выводятся как val.
И то же самое выполняется во внешней части функции как val
Это показано в этих двух строках:

in   in   in   in  
out  out  out  out 

Обратите внимание, что реальное изменение было выполнено в последнем eval функции.
Вот и все, готово. Теперь var имеет массив значений.

Фактически, ядро функции состоит из одной строки: eval $VarName\=\(${!VarName}\)

Кроме того, значение IFS установлено как локальное для функции, что заставляет его вернуться к значению, которое оно имело до выполнения функции, без дополнительной работы. Спасибо Peter Cordes за комментарий к оригинальной идее.

На этом объяснение закончено, надеюсь, все понятно.


Реальная функция

Если мы удалим все ненужные строки, оставив только ядро eval, и создадим новую переменную для IFS, мы сократим функцию до минимального выражения:

delim_to_array() {
    local IFS="${2:-$' :|'}"
    eval $1\=\(${!1}\);
}

Установка значения IFS в качестве локальной переменной позволяет нам также установить значение "по умолчанию" для функции. Всякий раз, когда значение, необходимое для IFS, не передается в функцию в качестве второго аргумента, локальная IFS принимает значение "по умолчанию". Мне показалось, что по умолчанию должны быть пробел ( ) (который всегда является полезным разделяющим значением), двоеточие (:) и вертикальная линия (|). Любое из этих трех значений будет разделять значения. Конечно, по умолчанию можно установить любые другие значения, которые соответствуют вашим потребностям.

Изменить для использования read:

Чтобы уменьшить риск некавычек в eval, можно использовать:

delim_to_array() {
    local IFS="${2:-$' :|'}"
    # eval $1\=\(${!1}\);
    read -ra "$1" <<<"${!1}"
}

test="fail-test"; a="fail-test"

animal_list='bison, a space, {1..3},~/,${a},$a,$((2+2)),$(echo "fail"),./*,*,*'

delim_to_array "animal_list" ","
printf "<%s>" "${animal_list[@]}"; echo

$ so-setvar.sh
< a space>< {1..3}><~/><${a}><$a><$((2+2))><$(echo "fail")><./*><*><*>

Большинство значений, заданных выше для var animal_list, не работают с eval.
Но проходят read без проблем.

  • Примечание: Совершенно безопасно пробовать вариант eval в этом коде, поскольку значения переменных были установлены в обычные текстовые значения непосредственно перед вызовом функции. Даже если они действительно выполнятся, они будут просто текстом. Это даже не проблема для файлов с неправильными именами, поскольку расширение имени пути является последним расширением, не будет никакого расширения переменных, повторно выполняемого поверх расширения имени пути. Опять же, с кодом как есть, это никоим образом не является проверкой для общего использования eval.

Пример

Чтобы действительно понять, что и как работает эта функция, я переписал код, который вы опубликовали, используя эту функцию:

#!/bin/bash

delim_to_array() {
        local IFS="${2:-$' :|'}"
        # printf "inside  IFS=<%s>\n" "$IFS"
        # eval $1\=\(${!1}\);
        read -ra "$1" <<<"${!1}";
}

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","
printf "NAME: %s\t " "${animal_list[@]}"; echo

people_list="alvin|baron|caleb|doug"
delim_to_array "people_list"
printf "NAME: %s\t " "${people_list[@]}"; echo

$ ./so-setvar.sh
NAME: anaconda   NAME:  bison    NAME:  cougar   NAME:  dingo    
NAME: alvin      NAME: baron     NAME: caleb     NAME: doug      

Как вы можете видеть, IFS устанавливается только внутри функции, он не изменяется постоянно, и поэтому его не нужно переустанавливать на старое значение. Кроме того, второй вызов функции "people_list" использует значение IFS по умолчанию, нет необходимости устанавливать второй аргумент.


"Here be Dragons " ¯\_(ツ)_/¯


Предупреждения 01:

Как была построена функция (eval), есть одно место, в котором var раскрывается без кавычек для разбора оболочки. Это позволяет нам получить "разделение слов", используя значение IFS. Но это также подвергает значения переменных (если только некоторые кавычки не предотвращают это): "расширение скобок", "расширение тильды", "расширение параметров, переменных и арифметических операций", "подстановка команды" и "расширение имени пути", в таком порядке. И подстановка процесса () в системах, которые ее поддерживают.

Пример каждой из них (кроме последней) содержится в этом простом echo (будьте осторожны):

 a=failed; echo {1..3} ~/ ${a} $a $((2+2)) $(ls) ./*

То есть, любая строка, которая начинается с {~$` или может соответствовать имени файла, или содержит ?*[], является потенциальной проблемой.

Если вы уверены, что переменные не содержат таких проблемных значений, то вы в безопасности. Если есть вероятность наличия таких значений, то способы ответа на ваш вопрос более сложны и требуют большего (даже более длинного) описания и объяснения. Использование read является альтернативой.

Предупреждения 02:

Да, read имеет свою долю "драконов".

  • Всегда используйте опцию -r, мне очень трудно придумать условие, при котором она не нужна.
  • Команда read может получить только одну строку. Многострочные, даже при установке опции -d, требуют особой осторожности. Иначе весь ввод будет присвоен одной переменной.
  • Если значение IFS содержит пробел, ведущие и последующие пробелы будут удалены. Ну, полное описание должно включать некоторые подробности о tab, но я их пропущу.
  • Не передавайте | данные для чтения. Если вы это сделаете, read будет находиться во вложенной оболочке. Все переменные, установленные во вложенной оболочке, не сохраняются при возврате в родительскую оболочку. Есть некоторые обходные пути, но, опять же, я пропущу подробности.

Я не хотел включать предупреждения и проблемы чтения, но по многочисленным просьбам пришлось их включить, извините.

6
09.03.2019, 16:15
2 ответа

Вы можете использовать класс символов [:punct:], если не возражаете, что он также соответствует другим знакам пунктуации и специальным символам:

grep '[[:punct:]]' file
6
27.01.2020, 20:21
grep "[]:/?#@\!\$&'()*+,;=%[]"

В выражении, заключенном в квадратные скобки, [...], очень немногие символы являются «специальными» (, только очень небольшое подмножество, например ], -и ^, и три комбинации [=, [:и [.). При включении ]в [...]]должно стоять первым (, возможно, после^). Я решил поставить ]первым, а [последним для симметрии.

Единственное, что следует помнить, это то, что строка в одинарных кавычках не может содержать одинарные кавычки, поэтому мы заключаем выражение в двойные кавычки. Поскольку мы используем строку в двойных кавычках, оболочка будет копаться в ней, чтобы что-то расширить. По этой причине мы экранируем $как \$, что заставит оболочку дать литерал $в grep, и мы также экранируем !как \!, так как это расширение истории вbash(только в интерактивных bashоболочках ).

Если вы хотите включить в набор обратную косую черту, вам нужно экранировать ее как \\, чтобы оболочка давала одну обратную косую черту для grep. Кроме того, если вы хотите включить обратную кавычку `, она также должна быть экранирована как \`, так как в противном случае она запускает подстановку команд.

Приведенная выше команда извлечет любую строку, содержащую хотя бы один из символов в выражении, заключенном в квадратные скобки.


Использование строки в одинарных кавычках вместо строки в двойных кавычках позволяет избежать большинства неприятностей, связанных с тем, какие символы интерпретирует оболочка:

grep '[]:/?#@!$&'"'"'()*+,;=%[]'

Здесь единственное, что нужно помнить, помимо размещения ], это то, что строка в одинарных кавычках не может включать одинарную кавычку, поэтому вместо этого мы используем конкатенацию трех строк:

  1. '[]:/?#@!$&'
  2. "'"
  3. '()*+,;=%[]'

Другим подходом может быть использование класса символов POSIX [[:punct:]]. Это соответствует одному символу из набора !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~, который является большим набором, чем то, что дано в вопросе (, он дополнительно содержит "-.<>^_`{|}~), но это все «знаки пунктуации», которые определяет POSIX..

LC_ALL=C grep '[[:punct:]]'
9
27.01.2020, 20:21

Теги

Похожие вопросы