Замена процесса для каждой записи массива без Eval - PullRequest
0 голосов
/ 26 июня 2018

У меня есть массив произвольных строк, например a=(1st "2nd string" $'3rd\nstring\n' ...).
Я хочу передать эти строки команде, которая интерпретирует свои аргументы как файлы, например paste.

Для фиксированного числа переменных мы можем использовать подстановку процесса

paste <(printf %s "$var1") <(printf %s "$var2") <(printf %s "$var3")

но это работает, только если число переменных известно заранее.
Для массива a мы могли бы написать что-то достаточно безопасное, например

eval paste $(printf '<(printf %%s %q) ' "${a[@]}")

Из интереса: есть ли способ обработать-заменить каждую из записей a без использования eval? Помните, что записи a могут содержать любой символ (кроме \0, потому что bash не поддерживает его).

Ответы [ 2 ]

0 голосов
/ 27 июня 2018

Это решение вдохновлено ответом Ричи .Это устраняет возможное столкновение имен, вызванное namerefs, но требует, чтобы пользователь указал разделитель, который не появляется в команде, которая будет выполнена.Тем не менее разделитель может появиться в массиве без проблем.

# Search a string in an array
# and print the 0-based index of the first identical element.
# Usage: indexOf STRING "${ARRAY[@]}"
# Exits with status 1 if the array does not contain such an element.
indexOf() {
    search="$1"
    i=0
    while shift; do
        [[ "$1" = "$search" ]] && echo "$i" && return
        ((++i))
    done
    return 1
}

# Execute a command and replace its last arguments by anonymous files.
# Usage: emulateFiles DELIMITER COMMAND [OPTION]... DELIMITER [ARGUMENT]...
# DELIMITER must differ from COMMAND and its OPTIONS.
# Arguments after the 2nd occurrence of DELIMITER are replaced by anonymous files.
emulateFiles() {
    delim="$1"
    shift
    i="$(indexOf "$delim" "$@")" || return 2
    cmd=("${@:1:i}")
    strings=("${@:i+2}")
    if [[ "${#strings[@]}" = 0 ]]; then
        "${cmd[@]}"
    else
        emulateFiles "$delim" "${cmd[@]}" <(printf %s "${strings[0]}") \
                     "$delim" "${strings[@]:1}"
    fi
}

Примеры использования

a=($'a b\n c ' $'x\ny\nz\n' : '*')
$ emulateFiles : paste : "${a[@]}"
a b x   :   *
 c  y       
    z       
$ emulateFiles : paste -d: : "${a[@]}"   # works because -d: != :
a b:x:::*
 c :y::
:z::
$ emulateFiles delim paste -d : delim "${a[@]}"
a b:x:::*
 c :y::
:z::
0 голосов
/ 26 июня 2018

Это пример того, как вы можете использовать рекурсию для настройки списка аргументов по одному аргументу за раз. Техника иногда полезна.

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

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

Bash 4.3 необходим для nameref (хотя вы можете сделать это с фиксированным именем массива, если вы еще не достигли этой версии). Namerefs требуют осторожности, потому что они не гигиеничны; локальная переменная может быть захвачена по имени. Отсюда и использование имен переменных, начинающихся с подчеркивания.

# A wrapper which sets up for the recursive call
from_array() {
  local -n _array=$1
  local -a _cmd=("${@:2}")
  local -i _count=${#_array[@]}
  from_array_helper
}

# A recursive function to create the process substitutions.
# Each invocation adds one process substitution to the argument
# list, working from the end.
from_array_helper() {
  if (($_count)); then
    ((--_count))
    from_array_helper <(printf %s "${_array[_count]}") "$@"
  else
    "${_cmd[@]}" "$@"
  fi
}

Пример

$ a=($'first\nsecond\n' $'x\ny\n' $'27\n35\n')
$ from_array a paste -d :
first:x:27
second:y:35
...