Как разделить вывод команды на отдельные строки

12
list=`ls -a R*`
echo $list

Внутри сценария оболочки эта команда echo выведет список всех файлов из текущего каталога, начиная с R, но в одной строке. Как я могу напечатать каждый элемент в одной строке?

Мне нужна общая команда для всех сценариев происходят с ls, du, find -type -dи т.д.

Anony
источник
6
Общее примечание: не делайте этого с ls, оно сломается на любых странных именах файлов . Смотрите здесь .
тердон

Ответы:

12

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

echo "$list"

В противном случае оболочка развернет и разделит содержимое переменных на пробелы (пробелы, новые строки, табуляции), и они будут потеряны.

Мур
источник
15

Вместо того, чтобы опрометчиво помещать lsвыходные данные в переменную, а затем использовать echoее, которая удаляет все цвета, используйте

ls -a1

От man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

Я не советую вам делать что-либо с выводом ls, кроме отображения :)

Используйте, например, оболочки оболочки и forцикл, чтобы сделать что-то с файлами ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* это не будет включать текущий каталог .или его родителя, ..хотя

Занна
источник
1
Я бы порекомендовал заменить: echo "$i"на printf "%s\n" "$i" (это не захлебнется, если имя файла начинается с "-"). На самом деле, большую часть времени echo somethingследует заменить на printf "%s\n" "something".
Оливье Дюлак
2
@OlivierDulac Все они начинаются Rздесь, но в целом да. Тем не менее, даже для сценариев попытка всегда учитывать ведущий -в путях вызывает проблемы: люди предполагают --, что только некоторые команды поддерживают, что означает конец опций. Я видел, find . [tests] -exec [cmd] -- {} \;где [cmd]не поддерживает --. Каждый путь там начинается с в .любом случае! Но это не аргумент против замены echoна printf '%s\n'. Здесь можно заменить весь цикл на printf '%s\n' R/*. Bash имеет printfвстроенную функцию, поэтому количество аргументов не ограничено.
Элия ​​Каган
12

В то время как положить его в кавычки , как @muru предложил действительно делать то , что вы просили, вы можете также рассмотреть возможность использования массива для этого. Например:

IFS=$'\n' dirs=( $(find . -type d) )

Команда IFS=$'\n'bash сообщает только о том, что нужно разделить вывод на символы новой строки, чтобы получить каждый элемент массива. Без этого он будет разделен на пробелы, поэтому a file name with spaces.txtбыло бы 5 отдельных элементов вместо одного. Этот подход сломается, если ваши имена файлов / каталогов могут содержать newlines ( \n). Это сохранит каждую строку вывода команды как элемент массива.

Обратите внимание , что я также изменил старый стиль `command`в $(command)котором предпочтительный синтаксис.

Теперь у вас есть массив с именем $dirsbof, каждый элемент которого является строкой вывода предыдущей команды. Например:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Теперь, из-за некоторой странности bash (см. Здесь ), вам нужно будет IFSвернуться к исходному значению после этого. Итак, либо сохраните его и переназначьте:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

Или сделайте это вручную:

IFS=" "$'\t\n '

Или просто закройте текущий терминал. Ваш новый будет иметь оригинальный набор IFS снова.

terdon
источник
Я мог бы предложить что-то подобное, но затем часть о том, как duэто выглядит, OP больше заинтересована в сохранении выходного формата.
Муру
Как я могу заставить это работать, если имена каталогов содержат пробелы?
десерт
@ десерты упс! Теперь он должен работать с пробелами (но не с символами новой строки). Спасибо за указание на это.
тердон
1
Обратите внимание, что вы должны сбросить или сбросить IFS после назначения этого массива. unix.stackexchange.com/q/264635/70524
Muru
@Muru Ого, спасибо, я забыла об этой странности.
Тердон
2

Если вам нужно сохранить выходные данные и вы хотите массив, mapfileэто легко.

Во-первых, подумайте, нужно ли вообще сохранять выходные данные вашей команды. Если нет, просто запустите команду.

Если вы решите, что хотите прочитать вывод команды в виде массива строк, это правда, что один из способов сделать это - отключить глобализацию, установить IFSразбиение на строки, использовать подстановку команд в ( )синтаксисе создания массива и выполнить сброс IFSпосле этого, что сложнее, чем кажется. Ответ Тердона охватывает некоторые из этого подхода. Но я предлагаю вам использовать встроенную команду mapfileоболочки Bash для чтения текста в виде массива строк. Вот простой случай, когда вы читаете из файла:

mapfile < filename

Это читает строки в массив с именем MAPFILE. Без перенаправления ввода вы будете читать из стандартного ввода оболочки (обычно вашего терминала) вместо файла с именем . Для чтения в массив, отличный от значения по умолчанию , передайте его имя. Например, это читает в массив :< filenamefilenameMAPFILElines

mapfile lines < filename

Другое поведение по умолчанию, которое вы можете изменить, состоит в том, что символы новой строки в концах строк остаются на месте; они появляются как последний символ в каждом элементе массива (если только ввод не завершился без символа новой строки, в этом случае последний элемент не имеет такового). Чтобы сжать эти новые строки, чтобы они не появлялись в массиве, передайте -tопцию mapfile. Например, это читает в массив recordsи не записывает завершающие символы новой строки в его элементы массива:

mapfile -t records < filename

Вы также можете использовать -tбез передачи имени массива; то есть он также работает с неявным именем массива MAPFILE.

mapfileОболочки встроенной опоры и другие варианты, и он в качестве альтернативы может быть вызван в качестве readarray. Запустите help mapfilehelp readarray) для деталей.

Но вы не хотите читать из файла, вы хотите читать из вывода команды. Чтобы добиться этого, используйте процесс подстановки . Эта команда читает строки из команды some-commandс arguments...аргументами командной строки и помещает их в mapfileмассив по умолчанию MAPFILEс удаленными завершающими символами новой строки:

mapfile -t < <(some-command arguments...)

Подстановка процесса заменяется реальным именем файла, из которого можно прочитать результаты выполнения. Файл является именованным каналом, а не обычным файлом, и в Ubuntu он будет именоваться как (иногда с другим номером, чем ), но вам не нужно заботиться о деталях, потому что оболочка заботится об этом все за кадром.<(some-command arguments...)some-command arguments.../dev/fd/6363

Вы можете подумать, что можете отказаться от замены процесса, используя вместо этого, но это не сработает, потому что, когда у вас есть конвейер из нескольких команд, разделенных Bash, запускает все команды в подоболочках . Таким образом оба и запускаются в своих собственных средах , инициализируемых, но отдельно от среды оболочки, в которой вы запускаете конвейер. В субоболочке , где работает, то массив имеет получить населенную, но затем этот массив отбрасываются , когда концы команд. никогда не создается и не изменяется для вызывающей стороны.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Вот как выглядит пример в ответе Тердона , если вы используете mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Вот и все. Вам не нужно проверять, IFSбыл ли он установлен , отслеживать, было ли оно установлено и с каким значением, устанавливать его на новую строку, а затем сбрасывать или сбрасывать его позже. Вам не нужно отключить подстановку (например, с set -f) , - которые действительно необходимы , если вы хотите использовать этот метод серьезно, так как имена файлов могут содержать *, ?и [--then повторно включить его ( set +f) впоследствии.

Вы также можете полностью заменить этот конкретный цикл одной printfкомандой - хотя это на самом деле не является преимуществом mapfile, так как вы можете сделать это независимо от того, используете ли вы mapfileили другой метод для заполнения массива. Вот более короткая версия:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

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

На самом деле не существует универсального решения.

Вы просили «общую команду для всех сценариев» и подход, основанный на использовании mapfileнескольких подходов к этой цели, но я призываю вас пересмотреть ваши требования.

Задача, показанная выше, лучше достигается с помощью одной findкоманды:

find . -type d -printf 'DIR: %p\n'

Вы также можете использовать внешнюю команду, например, sedдобавить DIR: в начало каждой строки. Это, возможно, несколько уродливо, и в отличие от этой команды find, она добавит дополнительные «префиксы» в имена файлов, содержащие символы новой строки, но она работает независимо от ее ввода, поэтому она как бы соответствует вашим требованиям и все же предпочтительнее считывать вывод в переменная или массив :

find . -type d | sed 's/^/DIR: /'

Если вам нужно перечислить, а также выполнить действие для каждого найденного каталога, например, запустить some-commandи передать путь к каталогу в качестве аргумента, то findвы тоже можете это сделать:

find . -type d -print -exec some-command {} \;

В качестве другого примера, давайте вернемся к общей задаче добавления префикса к каждой строке. Предположим, я хочу увидеть результат, help mapfileно нумеровать строки. Я бы на самом деле не использовал mapfileдля этого ни какой-либо другой метод, который считывает его в переменную оболочки или массив оболочки. Предположим help mapfile | cat -n, не дает нужного мне форматирования, я могу использовать awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

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

Альтернатива, которую обычно пытаются (а иногда и делают правильно), - читать входные данные построчно read -rв цикле. Если вам не нужно сохранять предыдущие строки при работе с более поздними строками, и вам нужно использовать длинный ввод, тогда это может быть лучше, чем mapfile. Но этого также следует избегать в тех случаях, когда вы можете просто передать его команде, которая может выполнить эту работу, что и происходит в большинстве случаев .

Элия ​​Каган
источник