Подстановка команд: разделение на новую строку, но не пробел

30

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

У меня есть файл с содержимым, как

AAA
B C DDD
FOO BAR

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

cmd AAA "B C DDD" "FOO BAR"

Если я использую, cmd $(< file)я получаю

cmd AAA B C DDD FOO BAR

и если я использую, cmd "$(< file)"я получаю

cmd "AAA B C DDD FOO BAR"

Как мне обработать каждую строку ровно одним параметром?

Old Pro
источник

Ответы:

26

Портабельно:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

Или используя подоболочку для IFSлокального изменения параметров:

( set -f; IFS='
'; exec cmd $(cat <file) )

Оболочка выполняет разбиение поля и генерацию имени файла в результате подстановки переменной или команды, которые не заключены в двойные кавычки. Таким образом, вам нужно отключить генерацию имени файла с помощью set -f, и настроить разделение полей, IFSчтобы сделать только новые строки отдельными полями.

С конструкциями bash или ksh мало что можно получить. Вы можете сделать IFSлокальным для функции, но нет set -f.

В bash или ksh93 вы можете хранить поля в массиве, если вам нужно передать их нескольким командам. Вы должны контролировать расширение во время создания массива. Затем "${a[@]}"расширяется до элементов массива, по одному на слово.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"
Жиль "ТАК - перестань быть злым"
источник
10

Вы можете сделать это с помощью временного массива.

Настроить:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Заполните массив:

$ IFS=$'\n'; set -f; foo=($(<input))

Используйте массив:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

Не могу найти способ сделать это без этой временной переменной - если только IFSизменение не имеет значения cmd, в этом случае:

$ IFS=$'\n'; set -f; cmd $(<input) 

должен сделать это.

Мат
источник
IFSвсегда сбивает меня с толку. IFS=$'\n' cmd $(<input)не работает IFS=$'\n'; cmd $(<input); unset IFSработает. Зачем? Я думаю, я буду использовать(IFS=$'\n'; cmd $(<input))
Old Pro
6
@OldPro IFS=$'\n' cmd $(<input)не работает, потому что он устанавливается только IFSв среде cmd. $(<input)раскрывается для формирования команды перед выполнением присваивания IFS.
Жиль "ТАК - перестань быть злым"
8

Похоже, канонический способ сделать это bashчто-то вроде

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

или, если ваша версия bash имеет mapfile:

mapfile -t args < filename
cmd "${args[@]}"

Единственное различие, которое я могу найти между mapfile и циклом while-read по сравнению с однострочным

(set -f; IFS=$'\n'; cmd $(<file))

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

Я бы использовал, IFS=$'\n' cmd $(<file)но это не работает, потому что $(<file)интерпретируется для формирования командной строки, прежде чем IFS=$'\n'вступит в силу.

Хотя в моем случае это не сработало, теперь я узнал, что многие инструменты поддерживают завершающие строки, null (\000)вместо newline (\n)которых многое облегчает работу, например, с именами файлов, которые являются распространенными источниками этих ситуаций. :

find / -name '*.config' -print0 | xargs -0 md5

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

tr "\n" "\000" <file | xargs -0 cmd

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

Old Pro
источник
Использование cmd $(<file)значений без кавычек (используя способность bash для разделения слов) всегда рискованно. Если есть какая-либо строка, *она будет расширена оболочкой до списка файлов.
3

Вы можете использовать Баш встроенный , mapfileчтобы прочитать файл в массив

mapfile -t foo < filename
cmd "${foo[@]}"

или, не проверенный, xargsможет сделать это

xargs cmd < filename
Гленн Джекман
источник
Из документации mapfile: «mapfile не является обычной или переносимой функцией оболочки». И действительно, это не поддерживается в моей системе. xargsтоже не помогает
Old Pro
Вам понадобится xargs -dилиxargs -L
Джеймс Янгман
@James, нет, у меня нет -dопции, и она xargs -L 1запускает команду по одному разу в строке, но все еще разделяет аргументы на пустом месте.
Old Pro
1
@OldPro, вы просили «способ сделать это, используя только встроенные средства bash» вместо «обычной или переносимой функции оболочки». Если ваша версия bash слишком старая, вы можете ее обновить?
Гленн Джекман
mapfileэто очень удобно для меня, так как захватывает пустые строки как элементы массива, чего IFSметод не делает. IFSобрабатывает смежные символы новой строки как один разделитель ... Спасибо за представление, поскольку я не знал о команде (хотя, основываясь на входных данных OP и ожидаемой командной строке, кажется, что он на самом деле хочет игнорировать пустые строки).
Peter.O
0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

Лучший способ найти. Просто работает.

Рахул Бали
источник
И почему это работает, если вы установили IFSв пространство, но вопрос заключается в том, чтобы не разделить на пространство?
Ральф Фридл
0

файл

Самый простой цикл (переносимый) для разделения файла на новые строки:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Который напечатает:

$ ./script
<AAA><A B C><DE F>

Да, по умолчанию IFS = spacetabnewline.

Почему это работает

  • IFS будет использоваться оболочкой для разделения входных данных на несколько переменных. Поскольку существует только одна переменная, оболочка не выполняет разбиение. Таким образом, никаких изменений не IFSтребуется.
  • Да, начальные и конечные пробелы / табуляции удаляются, но в данном случае это не проблема.
  • Нет, нет подстановки не делается , как никакое расширения не обращающаяся на рынке . Так что не set -fнужно.
  • Единственный используемый массив (или необходимый) - это позиционные параметры, подобные массиву.
  • Опция -r(raw) состоит в том, чтобы избежать удаления большей части обратной косой черты.

Это не будет работать, если необходимо разделение и / или сглаживание. В таких случаях требуется более сложная структура.

Если вам нужно (все еще портативный):

  • Избегайте удаления начальных и конечных пробелов / вкладок, используйте: IFS= read -r line
  • Сплит линия Варс на некоторый характер, использование: IFS=':' read -r a b c.

Разделить файл на какой-нибудь другой символ (не переносимый, работает с ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

расширение

Конечно, заголовок вашего вопроса о разделении выполнения команды на новые строки, избегая разделения на пробелы.

Единственный способ получить разбиение из оболочки - это оставить расширение без кавычек:

echo $(< file)

Это контролируется значением IFS, и к расширениям без кавычек также применяется глобализация. Чтобы сделать эту работу, вам необходимо:

  • Набор МФС к новой линии только , чтобы получить разделение на новой строке только.
  • Снимите опцию globbing shell set +f:

    set + f IFS = '' cmd $ (<файл)

Конечно, это меняет значение IFS и глобализации для остальной части сценария.

Исаак
источник