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


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.