Перебор имен файлов из конвейера в bash

Считайте меня расстроенным... Последние 2 часа я провел, пытаясь понять, как заставить команду, в которой есть каналы, перекачивать этот вывод в цикл for. Краткий рассказ о том, что я пытаюсь сделать, а затем мой код.

Я использую xbmc в течение многих лет. Однако вскоре после того, как я начал, я экспортировал свою библиотеку, что оказалось скорее хлопотным, чем стоящим (особенно сейчас, когда я использую установленную схему именования папок и файлов, содержащихся в них). Я хочу удалить все файлы, добавленные xbmc, поэтому я решил написать скрипт, который удалит все необходимые файлы. Однако тут я столкнулся с проблемой.

Я пытаюсь использовать команду locate (из-за ее скорости), за которой следует grep (чтобы избавиться от всей файловой системы .tbn) и egrep (чтобы удалить папку .actors, которую xbmc создает из результатов), за которой следует команда sort (хотя сортировка не обязательна, я добавил ее во время отладки, чтобы вывод при тестировании был лучше). Проблема в том, что обрабатывается только первый файл, а дальше ничего. Я много читал в Интернете и понял, что bash создает новую подоболочку для каждого канала, и к тому времени, когда он один раз завершит цикл, переменная уже мертва. Так что я больше копался в том, как обойти это, и все, казалось, показывало, как я могу обойти это для циклов while, но ничего для циклов for.

Хотя мне нравится думать, что я компетентен в написании сценариев, у меня всегда возникают такие вещи, которые доказывают, что я все еще только изучаю основы. Любая помощь от людей умнее меня будет принята с благодарностью.

#!/bin/bash

for i in "$(locate tbn | grep Movies | egrep -v .actors | sort -t/ +4)"
do
  DIR=$(echo $i | awk -F'/' '{print "/" $2 "/" $3 "/" $4 "/" $5 "/"}')
  rm -r "$DIR*.tbn" "$DIR*.nfo" "$DIR*.jpg" "$DIR*.txt" "$DIR.actors"
done

Прочитав ответ ниже, я думаю, что лучший способ выполнить то, что я хочу, заключается в следующем. Буду рад любым советам по новому сценарию. Вместо того, чтобы просто копировать и вставлять сценарий @Charles Duffy, я хочу найти правильный / лучший способ сделать это в качестве учебного опыта, поскольку всегда есть лучший и лучший способ что-то закодировать.

#!/bin/bash

for i in "*.tbn" "*.nfo" "*.jpg" "*.txt" "*.rar" #(any other desired extensions)
do
  find /share/movies -name "$i" -not -path "/share/movies/.actors" -delete
done

Сначала у меня есть часть -not -path, чтобы удалить из вывода папку .actors, которую xbmc помещает в корень исходного каталога (в данном случае /share/movies), чтобы оттуда не удалялись эскизы (файлы .tbn), но я хочу, чтобы они были удалены из любых других каталогов, содержащихся в /share/movies (и я хотел бы удалить миниатюры из папки .actors, если она содержится в определенной папке фильма). Вариант -delete связан с тем, что на странице gnu.org было предложено, что -delete лучше, чем вызов /bin/rm из-за отсутствия необходимости разветвления для процесса rm, что делает работу более эффективной и предотвращает накладные расходы.

Я почти уверен, что хочу, чтобы элементы в строке for были заключены в кавычки, поэтому в команде find используется буквальное значение *.tbn. Чтобы дать вам представление о структуре каталогов, это довольно просто. Я хочу удалить любой из файлов *.tbn *.jpg и *.nfo в этих каталогах.

/share/movies/movie 1/movie 1.mkv  
/share/movies/movie 1/movie 1.tbn  
/share/movies/movie 1/movie 1.jpg  
/share/movies/movie 1/movie 1.nfo  

/share/movies/movie 2/movie 2.mp4  
/share/movies/movie 2/movie 2.srt  
/share/movies/movie 2/movie 2 (subs).rar  

/share/movies/movie 3/movie 3.avi  
/share/movies/movie 3/movie 3.tbn  
/share/movies/movie 3/movie 3.jpg  
/share/movies/movie 3/movie 3.nfo  
/share/movies/movie 3/.actors/actor 1.tbn  
/share/movies/movie 3/.actors/actor 2.tbn  
/share/movies/movie 3/.actors/actor 3.tbn  

person bassmadrigal    schedule 26.09.2014    source источник
comment
См. запись BashPitfalls № 1: mywiki.wooledge.org/BashPitfalls   -  person Charles Duffy    schedule 27.09.2014
comment
Я бы не беспокоился о скорости; Сначала приходит правильно, потом приходит быстро, и к тому же вы планируете сделать это только один раз. Я бы использовал find или, может быть, xargs.   -  person Beta    schedule 27.09.2014
comment
Кстати, что вы пытаетесь сделать с помощью команды awk? Я не уверен, почему вы не можете добиться того же эффекта с расширением встроенного параметра bash.   -  person Charles Duffy    schedule 27.09.2014
comment
... если вы хотите обрезать начальный элемент каталога из имени, например: dir=/${i#*/}   -  person Charles Duffy    schedule 27.09.2014
comment
Вау... читая эту страницу BashPitfalls, я понимаю, что виноват во многих из них, но у меня никогда не было настоящих инструкций по программированию, и я просто учился с помощью Google на протяжении многих лет. Спасибо! С помощью команды awk я пытался удалить фактическое имя файла (последняя переменная, которая была бы $6) и сохранить остальную часть структуры каталогов нетронутой, чтобы затем использовать каталог в качестве базы для команды rm. Что касается вашего последнего комментария, что он делает? Без названия того, что он делает, Google не помог ... Я хотел бы изучить альтернативы, поскольку awk потребляет много энергии.   -  person bassmadrigal    schedule 29.09.2014
comment
@bassmadrigal, хорошо, если вы хотите удалить последний элемент каталога, это dir=${i%/*}. См. BashFAQ № 73: mywiki.wooledge.org/BashFAQ/073.   -  person Charles Duffy    schedule 29.09.2014
comment
Вы правы - если вы хотите передать подстановочные знаки, чтобы найти, вы действительно хотите их процитировать. Лично я бы более подробно рассказал о своих операторах группировки.   -  person Charles Duffy    schedule 29.09.2014


Ответы (3)


Ваш второй сценарий, отредактированный в вопросе, является улучшением. Тем не менее, есть еще возможности для улучшения:

#!/bin/bash

exts=( tbn nfo jpg txt rar )

find_args=( )    
for ext in "${exts[@]}"; do
  find_args+=( -name "*.$ext" -o )
done

find /share/movies -name .actors -prune -o \
 '(' "${find_args[@]:0:${#find_args[@]} - 1}" ')' -delete

Это создаст команду вида:

find /share/movies -name .actors -prune -o \
  '('    -name '*.tbn' -o -name '*.nfo' -o -name '*.jpg' \
      -o -name '*.txt' -o -name '*.rar' ')' -delete

...и, таким образом, обработать все расширение за один проход.

person Charles Duffy    schedule 29.09.2014
comment
Ладно, кажется, я понимаю, что здесь происходит. Вы устанавливаете расширения, которые мне нужны, в массиве с именем exts (все они являются отдельными элементами, поскольку все это не заключено в кавычки, верно?). Затем вы создаете пустой массив find_args. Теперь я немного запутался. Я считаю, что += добавляет -name "*.$ext" -o (с расширенной переменной) в конец find_args, поскольку он проходит через цикл for. Это правильно? И это добавляет три отдельных элемента каждый раз (поскольку есть пустое место и нет кавычек, охватывающих все)? - person bassmadrigal; 29.09.2014
comment
Я предполагаю, что часть массива в команде find получает первый индекс, а затем переходит и получает все, кроме последнего индекса (который не включает -o, что может вызвать проблемы, поскольку впоследствии ему не с чем сравнивать), но у меня есть понятия не имею, как это происходит. Я немного поискал в руководстве по bash, на которое вы ссылались, но я не совсем его понимаю. Я думаю, что он получает первый индекс со смещением 0, а затем получает последний индекс и вычитает 1, но я думаю, что #find_args обозначает какое-то сопоставление с образцом, но я не знаю что. - person bassmadrigal; 29.09.2014
comment
Теперь, правильный ли мой последний комментарий об оценке использования массива в команде find или нет, возникает вопрос. В какой момент не стоит усложнять задачу сделать что-то более эффективным, чем простое для чтения и многократное выполнение команды find? Может быть, если вы сканируете большой объем данных, это может занять много времени? Причина, по которой я спрашиваю, заключается в том, что я изначально был озадачен тем, что делает ваш код, пока я не потратил около часа на его разбор, чтобы надеяться, что я его понимаю. Однако я не думаю, что смог бы написать что-то подобное с моим уровнем знаний. - person bassmadrigal; 29.09.2014
comment
@bassmadrigal, это не совпадение с образцом; ${#array[@]} оценивается как количество элементов в массиве, так же как ${#string} оценивается как количество символов в строке. - person Charles Duffy; 29.09.2014
comment
@bassmadrigal, ... и точно - для большого количества данных запуск find несколько раз может занять некоторое время (особенно если каталоги больше, чем поместится в кеш вашей операционной системы, или если есть нехватка памяти, уменьшающая размер этого кеш). - person Charles Duffy; 29.09.2014
comment
@bassmadrigal, кстати, поскольку ваше редактирование показывает примеры фактической структуры каталогов, я изменил команду find, чтобы она стала более эффективной (используя -prune, что в первую очередь предотвращает рекурсию вниз по .actors, вместо использования -not -path для исключения пути туда после рекурсии в них). - person Charles Duffy; 29.09.2014
comment
@bassmadrigal: Чем больше вы делаете такие вещи, тем легче становится. Конечно, для одноразовой команды стоит так усердно работать над ней, только если вы хотите узнать больше об этом инструменте (если только команда не будет развернута на ферме серверов). Но преимущество того, что вы сделали это, состоит в том, что в следующий раз, когда у вас возникнет подобная проблема, очевидно сложное решение само придет вам в голову. Есть общие шаблоны и идиомы, и со временем они становятся частью вашего личного набора инструментов. - person rici; 30.09.2014
comment
@CharlesDuffy: Другое решение, если у вас есть gnu find и ни одно расширение не содержит пробельных символов, это: exts="tbn nfo jpg txt rar"; find ... -regex '.*\.'"${exts// /\\|}". Я не знаю, проще это или нет :) И, конечно, соответствует оригиналу: find ... -name "*.tbn" -execdir rm "*.tbn" "*.nfo" "*.jpg" "*.txt" "*.rar" \; -- хотя это немного странно в каталогах с более чем одним файлом .tbn, но, возможно, для этого есть решение. - person rici; 30.09.2014
comment
@rici, последний -execdir работает? Я думаю, что для расширения подстановочных знаков потребуется оболочка, поэтому вам понадобится что-то вроде -execdir sh -c 'rm *.tbn *.nfo *.jpg *.txt *.rar'. - person Charles Duffy; 30.09.2014
comment
@charles: хорошая мысль; -execdir не вставляет оболочку. С оболочкой можно сделать лучше: -execdir bash -c 'root=${1##*/};root=${root%.tbn}; rm "$root.tbn" "$root.nfo" "$root.jpg" "$root.txt" "$root.rar"' _ {} \;. Или так, как вы сделали это в том комментарии :) - person rici; 30.09.2014

Это просто проблема цитирования. "$(locate tbn | ...)" — это одно слово, потому что кавычки предотвращают разделение слов. Если вы опустите кавычки, это станет несколькими словами, но тогда пробелы в путях к файлам станут проблемой.

Лично я бы использовал find с предложением -exec; это может быть медленнее, чем locate (locate использует периодически обновляемую базу данных, поэтому точность жертвует скоростью), но это позволит избежать проблемы с цитированием.

person rici    schedule 26.09.2014
comment
Здесь проблема не только в кавычках — есть также подстановочные знаки в кавычках, которые пользователь ожидает расширить, и расширение перед echo, которое должно быть заключено в кавычки, но не заключено в кавычки. - person Charles Duffy; 27.09.2014
comment
... и, действительно, простого удаления кавычек недостаточно, поскольку разделение имен файлов на строки пробелами нежелательно. - person Charles Duffy; 27.09.2014
comment
@CharlesDuffy: я сказал это. Вы третье предложение не читали? - person rici; 27.09.2014
comment
Ах. Да, вы сделали. По-прежнему оставляет расширение глобуса непокрытым. - person Charles Duffy; 27.09.2014
comment
Я не вижу выполненных проблем с моим $i без кавычек при настройке переменной DIR. На самом деле он отображает желаемый результат, когда я возвращаю $DIR после запуска команды. Это просто удачный выход в моем случае, но обычно вызывает проблемы? - person bassmadrigal; 29.09.2014
comment
Что касается команды find -exec, прочитав о ней больше, вы предлагаете мне просто полностью удалить скрипт или изменить его на цикл for с расширениями, которые я хочу удалить, как $i, а затем использовать find и exec после этого удалить эти файлы непосредственно в команде поиска? find . -name *"$i" -exec /bin/rm {}; или на самом деле удалить напрямую find . -name *"$i" -delete ($i будет содержать расширение, которое я хочу удалить, которое будет установлено в цикле for, и мне понадобится подстановочный знак без кавычек и переменная в кавычках, верно?) - person bassmadrigal; 29.09.2014
comment
@bassmadrigal: Дело не в том, что $i не заключено в кавычки, хотя это проблема: дело в том, что $(...) заключено в кавычки, что означает, что i будет принимать только одно значение, которое это весь список. Поскольку вы не заключаете $i в кавычки, список предоставляется awk в виде одной строки, но он заботится только о первых нескольких полях, поэтому большая часть этой строки будет проигнорирована. - person rici; 29.09.2014
comment
@bassmadrigal: что касается команды find, я бы начал с find Music (или что-то в этом роде), чтобы вы искали в правильном каталоге. Кроме того, я не знаю точно, что вы пытаетесь сделать, но обычно вы указываете "*" в опции find -name, потому что идея состоит в том, чтобы find интерпретировал шаблон, а не оболочку. Если вы хотите удалить все файлы с расширением .nfo, вы можете сделать это, выполнив поиск -iname "*.nfo", но если вы хотите удалить foo.nfo из каталога, содержащего foo.tbn, вам понадобится что-то более сложное. - person rici; 29.09.2014
comment
О, я думаю, я никогда не понимал, что на самом деле делает цикл for. Я думал, что он будет передавать элементы в строке for по одному в сам цикл, поэтому в первый раз он будет передавать первый каталог, в следующий раз он будет передавать второй и так далее. Видимо, мне еще многому предстоит научиться... - person bassmadrigal; 29.09.2014
comment
Что касается вашего второго ответа, я редактирую свой первый пост, чтобы добавить новый скрипт на основе команды find. Однако я надеюсь, что смогу использовать цикл for, чтобы мне не приходилось постоянно изменять содержимое команды find. Поэтому я пытаюсь использовать то, что @CharlesDuffy упомянул в своем первом ответе, чтобы не * цитировать. Как только я отредактирую свой первый пост, посмотрите, что вы предлагаете оттуда. Судя по моему тестированию, он работает так, как я его опубликовал (но это, похоже, не обязательно означает, что это правильный способ делать что-то). - person bassmadrigal; 29.09.2014
comment
@bassmadrigal: это именно то, что делает цикл for. Но что такое предмет? Выражение в кавычках (если в нем нет символа @) является отдельным элементом, поскольку оно заключено в кавычки. Таким образом, между for a in $(seq 10); do echo loop; echo $a; done и for a in "$(seq 10)"; do echo loop; echo $a; done есть разница, которую вы легко увидите, если наберете эти две команды. - person rici; 29.09.2014
comment
@bassmadrigal, кстати, в отношении того, что echo $i без кавычек не вызывает проблем - попробуйте имя каталога с двумя пробелами рядом друг с другом или имя, содержащее звездочки, окруженные пробелами с обеих сторон. Это может сработать в большинстве случаев, но точно не во всех. - person Charles Duffy; 29.09.2014
comment
@rici, понятно. Таким образом, если элементы в цикле for заключены в кавычки, он внезапно становится одним элементом с новой строкой в ​​конце каждой записи. Пока они остаются без кавычек, каждое передается индивидуально. - person bassmadrigal; 29.09.2014
comment
@CharlesDuffy, это имеет смысл. Я довольно строг в своих стандартах именования фильмов, но я понимаю, что такие дополнительные вещи могут вызвать проблему. Я думаю, что мой пересмотренный сценарий в верхнем посте (после публикации) будет намного лучше благодаря всем предложениям, которые я получил здесь). - person bassmadrigal; 29.09.2014
comment
@bassmadrigal: элементы без кавычек разделяются пробелами (например, пробелами), а не только символами новой строки. Так что, вероятно, вам не нужна ни цитируемая, ни нецитируемая версия. Что было моей точкой зрения с самого начала. - person rici; 29.09.2014
comment
@rici, хорошо, это имеет смысл, почему мне следует избегать этого. Я вижу, как это может привести к множеству проблем. С тех пор я обновил свой первоначальный пост сильно измененным сценарием. Я думаю, что из-за проблем, ранее упомянутых здесь, я должен использовать свой второй (технически третий) цикл for, за исключением того, что я думаю, что мне, вероятно, следует заключать в кавычки каждый элемент в цикле for for i in "*.tbn" "*.jpg" #etc, чтобы предотвратить наличие * без кавычек в команду find, и я бы предположил, что это просто хорошая практика - заключать элементы цикла for в кавычки. Я ужасно не в себе? - person bassmadrigal; 29.09.2014
comment
@bassmadrigal, for i in "*.tbn" "*.jpg" помещает литеральную строку *.tbn в i, а не файлы, соответствующие *.tbn. Это, вероятно, не то, что вы хотите. for i in *.tbn *.jpg, напротив, перебирает файлы, соответствующие этим глобусам, а не сами глобы. - person Charles Duffy; 29.09.2014
comment
@bassmadrigal, ... теперь, если вы перебираете сами выражения glob, а затем используете их без кавычек позже, тогда они будут расширены до фактических имен файлов для тех случаев использования без кавычек. Что может быть, а может и не быть тем, что вы хотите, в зависимости от деталей написания вашего сценария. - person Charles Duffy; 29.09.2014
comment
@CharlesDuffy, хорошо, первый комментарий имеет смысл. Помещение их в кавычки делает их буквальной строкой для соответствия. Кроме того, кажется, что после прочтения этого подробного руководства по bash разделение слов выполняется до использования подстановочного знака, поэтому *.tbn и *.jpg должно быть достаточно. Однако меня смущает то, что вы имеете в виду в своем последнем комментарии. Вы хотите сказать, что если я оставлю его без кавычек в цикле for, он покажет some file.jpg как два файла? some и file.jpg? Это просто для того, чтобы убедиться, что я цитирую любое использование в реальном цикле, верно? - person bassmadrigal; 29.09.2014
comment
На самом деле, оглядываясь назад на мой сценарий, разве буквальная строка *.tbn не является тем, что мне нужно в команде поиска? Я просто использую цикл for для передачи расширений, которые я хочу найти, с помощью команды find внутри цикла. Я не хочу, чтобы фактические имена файлов передавались с помощью цикла for (по крайней мере, так, как это написано выше), потому что я хочу, чтобы фактическая команда поиска была find /share/movies -name "*.tbn" -not -path "/share/movies/.actors". Я снова обновил вторую половину моего вопроса с (надеюсь ) лучшее объяснение того, что я хочу сделать, и я думаю, что правильный сценарий для этого. - person bassmadrigal; 29.09.2014
comment
@bassmadrigal, *.jpg без кавычек дает одну строку, some file.jpg, потому что расширение глобуса делает все правильно. С другой стороны, если вы возьмете это имя, поместите его в переменную и развернете эту переменную без кавычек снова, вы получите some и file.jpg. Таким образом, вы хотите избежать кавычек на символах подстановки в то время, когда подстановка должна быть развернута, но только в это время. - person Charles Duffy; 29.09.2014
comment
Хорошо, это имеет смысл. Спасибо! - person bassmadrigal; 29.09.2014

Чтение имен файлов из locate в скрипте в целом является плохой новостью, если только ваша команда locate не имеет опции для разделения имен NUL (поскольку в имени файла допустим каждый символ, кроме NUL или /, новые строки действительно допустимы в именах файлов, что делает вывод locate неоднозначным ). Это сказало:

#!/bin/bash
# ^^ -- not /bin/sh, since we're using bash-only features here!

while read -u 3 -r i; do
  dir=${i%/*}
  rm -r "$dir/"*".tbn" "$dir/"*".nfo" "$dir/"*".jpg" "$dir/"*".txt" "$dir/.actors"
done 3< <(locate tbn | grep Movies | egrep -v .actors)

Обратите внимание, что *s не могут находиться внутри двойных кавычек, если вы хотите, чтобы они были развернуты, хотя имена каталогов должны быть заключены в двойные кавычки, если они пробелы и т. д. в их именах.


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

person Charles Duffy    schedule 26.09.2014
comment
Еще раз спасибо за достоверную информацию. Я всегда поражаюсь, когда осознаю, как мало я знаю о сценариях оболочки, даже несмотря на то, что мне удавалось делать впечатляющие (для меня) вещи. Есть ли причина использовать цикл while вместо цикла for? С первых дней своего обучения я узнал, что цикл for — отличный способ пройтись по списку вещей, включая файлы. Является ли цикл while лучше только потому, что я могу выполнить цикл команд, чтобы предотвратить появление проблемы подоболочки? Или есть еще более глубокая причина использовать цикл while вместо цикла for? - person bassmadrigal; 29.09.2014
comment
@bassmadrigal, while read -r считывает содержимое из потока locate построчно и может начать, как только locate начинает выдавать вывод (хотя использование sort заставляет вещи ждать конца, как это сделал бы for, и поэтому теряет это эффективность). Гораздо важнее то, что while read не разбивает слова и не расширяет подстановочные знаки, поэтому имена, содержащие пробелы или подстановочные знаки, обрабатываются безопасно. - person Charles Duffy; 29.09.2014
comment
@bassmadrigal, ...for безопасно, если вы перебираете содержимое массива или результаты выражения glob. Это не безопасно, если вы выводите подстановку команды с разбиением на строки, если не соблюдать осторожность (отключение расширения глобуса и настройка IFS так, чтобы она содержала только символы-разделители). - person Charles Duffy; 29.09.2014
comment
@bassmadrigal, ... см. также BashFAQ #1, в котором обсуждаются лучшие практики построчного чтения ввода: mywiki.wooledge.org/BashFAQ/001 - person Charles Duffy; 29.09.2014
comment
Мне определенно нужно больше копаться там. Я должен буду проверить это завтра, чтобы я мог лучше с этим. - person bassmadrigal; 29.09.2014
comment
Поскольку вы описали, что вы делаете с помощью awk, я удалил его из своего ответа здесь в пользу эквивалентного расширения параметра. - person Charles Duffy; 29.09.2014
comment
@bassmadrigal, ... также я удалил sort, так как он останавливает выполнение остальной части вашего скрипта до тех пор, пока не завершится locate. Это не имеет большого значения для locate, так как он быстрый, но будет иметь большое значение для find. Хотя, если бы вы собирались использовать find, использование -execdir и написание скрипта для запуска в целевых каталогах имело бы больше смысла. - person Charles Duffy; 29.09.2014
comment
Справедливо. Есть какие-нибудь мысли о моем обновленном сценарии (втором) в исходном вопросе? Это лучший способ сделать это? Я пытался избежать этого ответа, так как кажется, что поиск с помощью каналов для greps для получения желаемого списка больше не кажется лучшим способом для этого (хотя теперь я понимаю, что он делает и как) . Я просто ищу лучший способ сделать это, и, похоже, это не использовать его. И когда я прочитал о -delete в find, кажется, что он имеет те же преимущества, что и -execdir, для предотвращения условий гонки. - person bassmadrigal; 29.09.2014
comment
Новый скрипт в вопросе работает, но он очень избыточен - вы можете проверить все шаблоны (и гораздо быстрее) всего одним вызовом find. - person Charles Duffy; 29.09.2014