Как я могу избежать пробелов в списке циклов bash?

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

Вот содержимое моего тестового каталога:

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

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

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

Здесь вывод:

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

Черри-Хилл и Нью-Йорк рассматриваются как 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
)

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

Cherry
Hill
New
York
City

Ответ 1

Во-первых, не делайте этого. Лучший подход - правильно использовать find -exec:

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

Другим безопасным подходом является использование списка NUL-terminated, хотя для этого требуется, чтобы ваша находка поддерживала -print0:

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

Вы также можете заполнить массив из find и передать этот массив позже:

# 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, которая не содержит символа пробела. Отключите globbing (set -f), чтобы предотвратить расширение строк, содержащих символы glob, такие как [], * или ?:

# 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 "[email protected]"; do
  printf '%s\n' "$d"
done

будет поддерживать разделение. Обратите внимание, что цитирование (и использование [email protected], а не $*) важно. Массивы также могут быть заполнены другими способами, такими как выражения glob:

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

Ответ 2

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

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

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

Ответ 3

Вот простое решение, которое обрабатывает вкладки и/или пробелы в имени файла. Если вам нужно иметь дело с другими странными символами в имени файла, таком как новые строки, выберите другой ответ.

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

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

Ответ 4

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

Ответ 5

Не хранить списки как строки; храните их в виде массивов, чтобы избежать путаницы в этом разграничении. Вот пример script, который будет либо работать во всех подкаталогах теста, либо в списке, указанном в его командной строке:

#!/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 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=("[email protected]")
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

Ответ 6

Почему бы просто не поставить

IFS='\n'

перед командой for? Это изменяет разделитель полей от < Пробел > < Вкладкa > <Newline> , чтобы просто <Newline>

Ответ 8

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

Ответ 9

ps, если речь идет только о пространстве во входе, то некоторые двойные кавычки работали плавно для меня...

read artist;

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

Ответ 10

Вы можете использовать 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

Ответ 11

Чтобы добавить к тому, что Джонатан сказал: используйте параметр -print0 для find в сочетании с xargs следующим образом:

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

Это приведет к выполнению команды command с соответствующими аргументами; каталоги с пробелами в них будут правильно процитировать (т.е. они будут переданы как один аргумент).

Ответ 12

Пришлось иметь дело с пробелами в дорожках. Наконец, я решил использовать рекурсию и for item in /path/*:

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

Ответ 13

#!/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 находятся в разных папках и имена папок также имеют белые пробелы. Мой выше script преобразует файлы .mov в файл .avi в той же самой папке. Я не знаю, поможет ли вам людям.

Случай:

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

Ура!

Ответ 14

Преобразовать список файлов в массив 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. Недостатком является то, что если вход длинный, вы можете превысить ограничения размера командной строки Bash или использовать большие объемы памяти.

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

Мне также не нравится установка IFS, это может испортить другой код.

Ответ 15

только что выяснили, что есть некоторые сходства между question и вашим. Поразительно, если вы хотите передать аргументы в команды

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

чтобы распечатать их в порядке

for SOME_ARG in "[email protected]"
do
    echo "$SOME_ARG";
done;

Обратите внимание, что [email protected]окружен двойными кавычками, некоторые примечания здесь

Ответ 16

Мне нужна была такая же концепция, чтобы последовательно сжимать несколько каталогов или файлов из определенной папки. Я решил использовать 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!!!"

как вы думаете?

Ответ 17

find Downloads -type f | while read file; do printf "%q\n" "$file"; done

Ответ 18

Ну, я вижу слишком много сложных ответов. Я не хочу передавать вывод утилиты find или писать цикл, потому что для этого есть опция "exec".

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

Я решил это так:

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

Выглядит очень просто для меня

Ответ 19

Для меня это работает, и это в значительной степени "чисто":

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

Ответ 20

У меня была простая проблема... Конвертируйте файлы набранных .flv в .mp3 (yawn).

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

рекурсивно найти все пользовательские флеш файлы Macintosh и превратить их в аудио (копировать, перекодировать)... это похоже на то, что указано выше, отметив, что чтение вместо "для файла в " исчезнет.