Существует ли «escape-конвертер» для имен файлов и каталогов? - PullRequest
8 голосов
/ 19 декабря 2009

Настал день, когда мне пришлось написать скрипт BASH, который обходит произвольные деревья каталогов и просматривает произвольные файлы и пытается определить что-то в отношении сравнения между ними. Я думал, что это будет простая пара часов вершин! процесс - не так!

Мое зависание в том, что иногда какой-то идиот -ахем! - извините, прекрасный пользователь выбирает ставить пробелы в именах каталогов и файлов. Это приводит к сбою моего сценария.

Идеальное решение , кроме угрозы гильотине для тех, кто настаивает на использовании пробелов в таких местах (не говоря уже о парнях, которые помещают это в код операционной системы!), Может быть рутиной, которая «экранирует» имена файлов и каталогов для нас, вроде того, как у cygwin есть процедуры для преобразования из unix в форматы имен файлов dos. Есть ли что-нибудь подобное в стандартном дистрибутиве Unix / Linux?

Обратите внимание, что простая конструкция for file in * не работает так хорошо, когда кто-то пытается сравнить деревья каталогов, так как она ONLY работает с "текущим каталогом" - и, в этом случае, как и во многих другие, постоянно переписывающиеся в различные каталоги, приносят свои проблемы. Итак, выполняя домашнее задание, я нашел этот вопрос Обработка специальных символов в bash для ... в цикле , и предлагаемое решение зависает на пробелах в именах каталогов, но может быть просто преодолено следующим образом:

dir="dirname with spaces"
ls -1 "$dir" | while read x; do
   echo $x
done

ОБРАТИТЕ ВНИМАНИЕ: Приведенный выше код не особенно хорош, потому что переменные, используемые внутри цикла while, недоступны вне цикла while. Это связано с тем, что подразумеваемая подоболочка создается, когда вывод команды ls передается по конвейеру. Это ключевой мотивирующий фактор для моего запроса!

... Хорошо, приведенный выше код помогает во многих ситуациях, но "экранирование" символов также будет довольно мощным. Например, приведенный выше каталог может содержать:

dir\ with\ spaces

Это уже существует, и я только что пропустил это?

Если нет, у кого-нибудь есть простое предложение написать его - может быть, с помощью sed или lex? (Я далеко не компетентен с любым.)

Ответы [ 6 ]

4 голосов
/ 20 декабря 2009

Создать действительно неприятное имя файла для тестирования:

mkdir escapetest
cd escapetest && touch "m'i;x&e\"d u(p\nmulti)\nlines'\nand\015ca&rr\015re;t"

[Редактировать: Скорее всего, я предполагал, что touch команда будет:

touch $'m\'i;x&e\"d u(p\nmulti)\nlines\'\nand\015ca&rr\015re;t'

, который помещает более ужасные символы в имя файла. Вывод будет выглядеть немного иначе. ]

Затем запустите это:

find -print0 | while read -d '' -r line; do echo -en "--[${line}]--\t\t"; echo "$line"|sed -e ':t;N;s/\n/\\n/;bt' | sed 's/\([ \o47()"&;\\]\)/\\\1/g;s/\o15/\\r/g'; done

Вывод должен выглядеть следующим образом:

--[./m'i;x&e"d u(p
multi)
lines'
re;t]--         ./m\'i\;x\&e\"d\ u\(p\\nmulti\)\\nlines\'\\nand\\015ca\&rr\\015re\;t

Состоит из сжатой версии монстра Pascal Thivent sed, плюс обработка возврата каретки и перевода строки и, возможно, немного больше.

Первый проход по sed объединяет несколько строк в одну, разделенную "\ n" для имен файлов, которые имеют переводы строк. Второй проход заменяет любой из списка символов обратным слешем перед собой. Последняя часть заменяет возврат каретки на "\ r".

Следует отметить, что, как вы знаете, while будет обрабатывать пробелы, а for - только отправляя вывод find с нулевым завершением и устанавливая разделитель read в ноль, Вы также можете обрабатывать переводы строк в именах файлов. Опция -r заставляет read принимать обратную косую черту без их интерпретации.

Редактировать:

Еще один способ экранирования специальных символов, на этот раз без использования sed, использует функцию цитирования и создания переменных встроенной функции Bash printf (это также иллюстрирует использование подстановки процесса, а не канала):

while read -d '' -r file; do echo "$file"; printf -v name "%q" "$file"; echo "$name"; done< <(find -print0)

Переменная $name будет доступна вне цикла, поскольку использование подстановки процесса предотвращает создание подоболочки вокруг цикла.

2 голосов
/ 20 декабря 2009

Существует довольно серьезная проблема с подходом экранирования: какие экранирования необходимы, зависит от контекста, в котором будет раскрываться переменная, и в обычном случае экранирование не сработает. Например, если вы собираетесь сделать что-то простое, например:

touch a "b c" d
files="a b\ c d"
ls $files

... это не сработает (ls ищет 4 файла: "a", "b \", "c" и "d"), потому что оболочка не обращает внимания на экранирование, когда произносит слово -сплит $ файлов. Вы можете использовать eval ls $files, но это не сработает в таких вещах, как вкладки в именах файлов.

Предложенный fgm подход while ... read ... done < <(find ... -print0) работает надежно (и из-за гибкости шаблонов поиска find очень эффективен), но это также довольно грязная куча обходных путей для различных возможных проблем; если вам не нужна сила поиска, не сложно сделать что-то с for и *:

shopt -s nullglob    # In case of empty directories...
for filepath in "$dir"/*; do    # loop over all files in the specified directory
    filename="${filepath##*/}"    # You just wanted the files' names?  No problem.
    echo "$filename"
done

Если (как вы упомянули в вопросе) вы заинтересованы в сравнении двух деревьев каталогов, циклическое переключение одного из них не совсем то, что вам нужно; было бы лучше поместить их содержимое в массивы, например так:

shopt -s nullglob
pathlist1=("$dir1"/*)    # Get a list of paths of files in dir1
filelist1=("${pathlist1[@]##*/}")    # Parse off just the filenames
pathlist2=("$dir2"/*)    # Same for dir2
filelist2=("${pathlist2[@]##*/}")
# now compare filelist1 with filelist2...

(Обратите внимание, что AFAIK конструкция "${pathlist2[@]##*/}" не является стандартной, но, похоже, уже некоторое время поддерживается в bash и zsh.)

2 голосов
/ 20 декабря 2009

Следующий фрагмент обрабатывает все имена файлов (включая пробелы, кавычки, новые строки, ...):

startdir="${1:-.}"                              # first parameter or working directory

#-------------------------------------------------------------------------------
#  IFS is undefined
#  read:
#  -r  do not allow backslashes to escape any characters
#  -d  delimiter is \0  (not a valid character in a filename)
#  done < <( find ... ) . redirection from a process substitution
#-------------------------------------------------------------------------------
while IFS=  read -r -d '' file; do
  echo "'$file'"
done < <( find "$startdir" -type f -print0 )

См. Также BashFAQ .

2 голосов
/ 20 декабря 2009

Я нашел это Как избежать имен файлов в сценариях оболочки bash во время поиска в Google, который я цитирую ниже:

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

Если у кого-то есть лучшее решение, пожалуйста, дай мне знать. Это работает, и это читаемый, но не симпатичный.

FILE_ESCAPED=`echo "$FILE" | \
sed s/\\ /\\\\\\\\\\\\\\ /g | \
sed s/\\'/\\\\\\\\\\\\\\'/g | \
sed s/\&/\\\\\\\\\\\\\\&/g | \
sed s/\;/\\\\\\\\\\\\\\;/g | \
sed s/\(/\\\\\\\\\\(/g | \
sed s/\)/\\\\\\\\\\)/g `

Может быть, вы могли бы использовать его в качестве отправной точки.

1 голос
/ 21 января 2010
#!/bin/bash

while read filename; do
  echo 'I am doing something with "'"$filename"'".'
done < <(find)

Обратите внимание, что нотация <( ) не будет работать, когда bash вызывается как /bin/sh.

0 голосов
/ 20 декабря 2009

Команда find иногда работает в этой ситуации:

find . -exec ls {} \;

например

...