Як я можу уникнути білого простору у списку циклів bash?


121

У мене є скрипт bash shell, який проходить через усі дочірні каталоги (але не файли) певного каталогу. Проблема полягає в тому, що деякі імена каталогів містять пробіли.

Ось вміст мого тестового каталогу:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

І код, який проходить через каталоги:

for f in `find test/* -type d`; do
  echo $f
done

Ось результат:

тест / Балтимор
тест / Вишня
Пагорб
тест / Едісон 
тест / Нові
Йорк
Місто
тест / Філадельфія

Cherry Hill та New York City розглядаються як 2 або 3 окремі записи.

Я спробував цитувати імена файлів, як-от так:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

але безрезультатно

Має бути простий спосіб зробити це.


Відповіді нижче чудові. Але щоб зробити це більш складним - я не завжди хочу використовувати каталоги, перелічені в моєму тестовому каталозі. Іноді я хочу передати імена каталогів як параметри командного рядка.

Я прийняв пропозицію Чарльза встановити IFS і придумав таке:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

і це працює чудово, якщо в аргументах командного рядка немає пробілів (навіть якщо ці аргументи цитуються). Наприклад, виклик сценарію таким чином: test.sh "Cherry Hill" "New York City"видає такий результат:

Вишня
Пагорб
Нові
Йорк
Місто

re: редагування, list="$@"повністю відкидає список-ності оригінального значення, згортаючи його на рядок. Будь ласка, дотримуйтесь практик у моїй відповіді точно так, як зазначено - таке завдання ніде не рекомендується; якщо ви хочете передати список аргументів командного рядка програмі, вам слід зібрати їх у масив та безпосередньо розгорнути цей масив.
Чарльз Даффі

Відповіді:


105

По-перше, не робіть цього так. Найкращий підхід - find -execправильно використовувати :

# this is safe
find test -type d -exec echo '{}' +

Іншим безпечним підходом є використання списку, що припиняється NUL, хоча для цього потрібна підтримка пошуку -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Ви також можете заповнити масив з пошуку та передати цей масив пізніше:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Якщо ваша знахідка не підтримує -print0, ваш результат тоді небезпечний - наведені нижче дані не будуть діяти так, як бажано, якщо існують файли, що містять нові рядки у своїх іменах (що так, законно):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Якщо хтось не збирається використовувати одне з вищезазначених, третій підхід (менш ефективний з точки зору використання часу і пам’яті, оскільки він читає весь вихід підпроцесу перед тим, як зробити розбиття слів) - це використовувати IFSзмінну, яка не не містить символу пробілу. Вимкніть підстановку ( set -f) , щоб запобігти рядки , що містять Глоби символи , такі як [], *або ?з розширюються:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Нарешті, для випадку параметра командного рядка ви повинні використовувати масиви, якщо ваша оболонка підтримує їх (тобто це ksh, bash або zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

збереже розлуку. Зауважте, що цитування (і використання, $@а не використання $*) є важливим. Масиви можуть бути заповнені і іншими способами, такими як глобальні вирази:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1
не знав про цей "+" аромат для -exec. солодке
Йоханнес Шауб - ліб

1
Тхо виглядає, що він також може, як і xargs, лише ставити аргументи в кінці даної команди: / що мене буває
помилка

Я думаю, що -exec [name] {} + - це розширення GNU та 4.4-BSD. (Принаймні, він не з’являється на Solaris 8, і я не думаю, що це було і в AIX 4.3.) Я думаю, що решта з нас може застрягти з трубопроводом до xargs ...
Michael Ratanapintha

2
Я ніколи раніше не бачив синтаксису $ '\ n'. Як це працює? (Я б міг подумати, що або IFS = '\ n', або IFS = "\ n" буде працювати, але так і не працює.)
MCS

1
@crosstalk це точно в Solaris 10, я просто використовував це.
Нік

26
find . -type d | while read file; do echo $file; done

Однак не працює, якщо ім'я файлу містить нові рядки. Наведене вище - єдине мені відоме рішення, коли ви насправді хочете мати ім'я каталогу у змінній. Якщо ви просто хочете виконати якусь команду, використовуйте xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '

Не потрібно xargs, див. Find -exec ... {} +
Чарльз Даффі,

4
@Charles: для великої кількості файлів xargs набагато ефективніше: він породжує лише один процес. Параметр -exec встановлює новий процес для кожного файлу, який може бути на порядок повільнішим.
Адам Розенфілд

1
Мені більше подобаються xargs. Ці два по суті, схоже, роблять те саме і те і інше, в той час як у xargs є більше варіантів, як паралельно бігати
Йоганнес Шауб - ліб

2
Адам, не те, що "+" зведе якнайбільше імен файлів, а потім виконає. але він не матиме таких акуратних функцій, як паралельний біг :)
Йоханнес Шауб - ліб

2
Зауважте, що якщо ви хочете щось зробити з іменами, вам доведеться їх цитувати. Напр .:find . -type d | while read file; do ls "$file"; done
Девід Молес

23

Ось просте рішення, яке обробляє вкладки та / або пробіли у назві файлу. Якщо вам доведеться мати справу з іншими дивними символами у назві файлу, як нові рядки, виберіть іншу відповідь.

Каталог тестів

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Код, який слід перейти в каталоги

find test -type d | while read f ; do
  echo "$f"
done

Ім'я файлу має бути цитується ( "$f"), якщо воно використовується як аргумент. Без лапок пробіли виконують функцію розділювача аргументів, а декілька аргументів надаються команді, що викликається.

І вихід:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

дякую, це працювало для псевдоніма, який я створював, щоб перелічити, скільки місця займає кожен каталог у поточній папці, він задихався на деяких dirs з пробілами в попередньому втіленні. Це працює в zsh, але деякі інші відповіді так і не зробили:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Тед Налейд

2
Якщо ви використовуєте zsh, ви також можете це зробити:alias duc='du -sh *(/)'
cbliard

@cbliard Це все ще баггі. Спробуйте запустити його з назвою файлу з, скажімо, послідовністю вкладки або кількома пробілами; ви зауважите, що він змінює будь-який з них в єдиний простір, тому що ви не цитуєте свого відлуння. І ось випадок назви файлів, що містять нові рядки ...
Чарльз Даффі

@CharlesDuffy Я спробував із послідовностями вкладок та кількома пробілами. Це працює з цитатами. Я також спробував з новими лініями, і це зовсім не працює. Відповідно я оновив відповідь. Дякую, що вказали на це.
cbliard

1
@cbliard Право - додавання цитат до вашої команди ехо було саме те, що я отримував. Що стосується нових рядків, ви можете зробити цю роботу за допомогою пошуку -print0і IFS='' read -r -d '' f.
Чарльз Даффі

7

Це вкрай складно в стандартному Unix, і більшість рішень не відповідають новим рядкам або іншим символам. Однак якщо ви використовуєте набір інструментів GNU, тоді ви можете використовувати findпараметр -print0і використовувати xargsвідповідний варіант -0(мінус-нуль). Є два символи, які не можуть відображатися в простому імені файлу; це похила та NUL '\ 0'. Очевидно, коса риса відображається в іменах шляхів, тому рішення GNU використовувати NUL '\ 0' для позначення кінця імені геніальне і нерозумне.


4

Чому б просто не поставити

IFS='\n'

перед командою for? Це змінює роздільник поля з <Пробіл> <Tab> <Newline> на просто <Newline>


4
find . -print0|while read -d $'\0' file; do echo "$file"; done

1
-d $'\0'точно так само, як -d ''- оскільки bash використовує рядки, що закінчуються NUL, перший символ порожнього рядка - це NUL, і з тієї ж причини NULs взагалі не можуть бути представлені всередині C рядків.
Чарльз Даффі

4

я використовую

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Хіба цього не вистачить?
Ідея взята з http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html


відмінний рада: це дуже корисно для опцій командного рядка osascript (OS X AppleScript), де пробілу розколоти аргумент на кілька параметрів , в яких тільки один призначений
тім

Ні, цього недостатньо. Це неефективно (через непотрібне використання $(echo ...)), не обробляє імена файлів глобусними виразами правильно, не обробляє імена файлів, які містять $'\b'символи або $ \ \ n ', а також перетворює декілька пробілів пробілу в окремі символи пробілу на вихідна сторона через неправильне котирування.
Чарльз Даффі

4

Не зберігайте списки як рядки; зберігайте їх як масиви, щоб уникнути всієї плутанини цього роздільника. Ось приклад скрипту, який буде працювати або в усіх підкаталогах тесту, або зі списку, поданого в його командному рядку:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Тепер давайте спробуємо це на тестовому каталозі з кривою або двома закинутими:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1
Озираючись на це - насправді було рішення з POSIX sh: Ви можете повторно використовувати "$@"масив, додавши до нього set -- "$@" "$f".
Чарльз Даффі

4

Ви можете використовувати IFS (внутрішній роздільник поля) тимчасово, використовуючи:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS


Будь ласка, надайте пояснення.
Стів К

IFS вказав, що таке символ розділювача, тоді ім'я файлу з пробілом не буде усічено.
дивовижно,

$ IFS = $ OLD_IFS наприкінці має бути: IFS = $ OLD_IFS
Michel Donais

3

ps якщо мова йде лише про місце у вході, то деякі подвійні лапки працювали для мене плавно ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;

2

Щоб додати те, що сказав Джонатан : використовуйте -print0параметр findу поєднанні з xargsнаступним:

find test/* -type d -print0 | xargs -0 command

Це виконає команду commandза допомогою належних аргументів; каталоги з пробілами в них будуть належним чином цитовані (тобто вони будуть передані як один аргумент).


1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Вищевказаний код перетворить .mov файли в .avi. Файли .mov містяться в різних папках, і в назвах папок теж є пробіли . Мій вище скрипт конвертуватиме файли .mov у файл .avi у тій самій папці. Я не знаю, чи допомагає вам народ.

Справа:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Ура!


echo "$name" | ...не працює, якщо nameє -n, і як він поводиться з іменами з послідовностями зворотної косої риси залежать від вашої реалізації - POSIX робить поведінку echoв цьому випадку явно невизначеним (тоді як розширений POS-розширення POSSI робить розширення послідовностей відхилення від косої риски стандартно визначеним поведінкою і GNU система - в тому числі Баша - без POSIXLY_CORRECT=1перерви стандарт POSIX по реалізації -e( в той час як специфікація потрібно echo -eнадрукувати -e. на виході) printf '%s\n' "$name" | ...безпечніше.
Чарльз Даффі

1

Довелося мати справу і з пробілами в назвах шляхів. Що я, нарешті, зробив, використовуючи рекурсію і for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}

1
Не використовуйте functionключове слово - це робить ваш код несумісним з POSIX sh, але не має іншої корисної мети. Ви можете просто визначити функцію за допомогою recursedir() {, додавши два паролі та видаливши ключове слово функції, і це буде сумісно з усіма сумісними оболонками POSIX.
Чарльз Даффі

1

Перетворіть список файлів у масив Bash. Для цього використовується підхід Метта МакКлура для повернення масиву з функції Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Результат - це спосіб для перетворення будь-якого багаторядкового введення в масив Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Цей підхід, здається, працює навіть тоді, коли наявні погані символи, і є загальним способом перетворення будь-якого вводу в масив Bash. Недоліком є ​​те, що якщо вхід довгий, ви можете перевищити обмеження розміру командного рядка Баша або використовувати велику кількість пам'яті.

Підходи, коли цикл, який врешті-решт працює у списку, також містить список, має недолік, що читання stdin непросте (наприклад, запит у користувача для введення даних), а цикл - це новий процес, тому вам може бути цікаво, чому змінні Ви встановите всередині циклу, недоступні після закінчення циклу.

Мені також не подобається налаштування IFS, це може зіпсувати інший код.


Якщо ви використовуєте IFS='' readв одному рядку, параметр IFS присутній лише для команди read і не уникає цього. Немає ніяких причин не любити налаштування IFS таким чином.
Чарльз Даффі

1

Ну, я бачу занадто багато складних відповідей. Я не хочу передавати висновок утиліти find або писати цикл, тому що find має для цього варіант "exec".

Моя проблема полягала в тому, що я хотів перемістити всі файли з розширенням dbf до поточної папки, а деякі з них містили пробіл.

Я вирішив це так:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Для мене це виглядає дуже просто


0

щойно я дізнався, що між моїм запитанням і вашим є деякі подібності . Якщо ви хочете передати аргументи командам

test.sh "Cherry Hill" "New York City"

роздрукувати їх по порядку

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

зауважте, що $ @ оточений подвійними котируваннями, деякі примітки тут


0

Мені потрібна була та сама концепція, щоб послідовно стискати кілька каталогів або файлів із певної папки. Я вирішив за допомогою awk для розбору списку від ls та уникнення проблеми порожнього простору в імені.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

що ти думаєш?


Я думаю, що це не спрацює правильно, якщо в назви файлів є нові рядки. Можливо, вам варто спробувати.
user000001


-3

Для мене це працює, і це майже «чисто»:

for f in "$(find ./test -type d)" ; do
  echo "$f"
done

4
Але це гірше. Подвійні лапки навколо знахідки змушують об'єднати всі назви шляхів у один рядок. Змініть відлуння на ls, щоб побачити проблему.
NVRAM

-4

Просто була проста проблема варіанту ... Перетворити файли набраного .flv в .mp3 (позіхання).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

рекурсивно знайдіть всі флеш-файли користувачів Macintosh та перетворіть їх на аудіо (копія, без перекоду) ... це як і раніше, зауваживши, що прочитане замість просто "для файлу в " не вдасться.


2
readПісля того, як inще одне слово в списку ви Перебір. Те, що ви опублікували, - це дещо зламана версія того, що ставив запитувач, а це не працює. Можливо, ви мали намір опублікувати щось інше, але це, мабуть, охоплене іншими відповідями тут все одно.
Жил 'ТАК - перестань бути злим'
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.