Підстановка команд: розділення на новий рядок, але не пробіл


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"

Як обробляти кожен рядок точно одним параметром?


Відповіді:


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[@]}"

Єдина відмінність, яку я можу знайти між картографічним файлом і циклом під час читання, порівняно з однолінійним

(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

Хоча це теж ігнорує порожні рядки, хоча це і фіксує рядки, які мають лише пробіли.


Використання cmd $(<file)значень без цитування (використання здатності bash розділяти слова) - це завжди ризикована ставка. Якщо будь-який рядок, *він буде розширений оболонкою до списку файлів.

3

Ви можете використовувати вбудований bash, mapfileщоб прочитати файл у масив

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

або, неперевірений, xargsможе це зробити

xargs cmd < filename

З документації на файл mapfile: "mapfile не є звичайною або портативною функцією оболонки". І справді це не підтримується в моїй системі. xargsтеж не допомагає.
— Старий Про

Вам знадобиться xargs -dабоxargs -L
— Джеймс Янгмен

@James, ні, у мене немає -dможливості і xargs -L 1виконувати команду один раз за рядком, але все ж розбиває аргументи на пробіл.
— Старий Про

1
@OldPro, добре, ви просили "спосіб це зробити, використовуючи лише вбудовані файли bash" замість "загальної або портативної функції оболонки". Якщо ваша версія bash занадто стара, чи можете ви її оновити?
— Глен Джекман

mapfileдля мене дуже зручно, оскільки він захоплює порожні рядки як елементи масиву, що IFSметод не робить. IFSтрактує суміжні нові рядки як єдиний роздільник ... Дякую за те, що я представив, як я не знав про команду (хоча, виходячи з вхідних даних OP та очікуваного командного рядка, схоже, він насправді хоче ігнорувати порожні рядки).
— Пітер.О

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(Сирий) варіант , щоб уникнути видалення більшої зворотної косої межі.

Це не спрацює, якщо потрібне розщеплення та / або глобалізація. У таких випадках потрібна більш складна структура.

Якщо вам потрібно (все ще портативно):

  • Уникайте видалення провідних та кінцевих пробілів / вкладок, використовуйте: IFS= read -r line
  • Спліт лінія Варс на певний тип, використання: IFS=':' read -r a b c.

Розділіть файл на інший символ (не портативний, працює з ksh, bash, zsh):

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

Розширення

Звичайно, назва вашого питання стосується розбиття виконання команди на нові рядки, уникаючи розбиття на пробіли.

Єдиний спосіб отримати розщеплення від оболонки - залишити розширення без лапок:

echo $(< file)

Це контролюється значенням IFS, а при розширеннях без котирування також застосовується глобалізація. Щоб зробити цю роботу, вам потрібно:

  • Набір МФС до нової лінії тільки , щоб отримати поділ на новому рядку тільки.
  • Скасуйте параметр оболонки з глобусом set +f:

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

Звичайно, це змінює значення IFS та глобалізації для решти сценарію.

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