Как я могу использовать обратные или отрицательные подстановочные знаки при сопоставлении с образцом в оболочке unix / linux?

267

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

cp [exclude-matches] *Music* /target_directory

Что следует делать вместо [exclude-matches] для выполнения этого?

Теги:
pattern-matching
glob

11 ответов

279
Лучший ответ

В Bash вы можете сделать это, включив опцию extglob, например (замените ls для cp и добавьте целевой каталог, конечно)

~/foobar> shopt extglob
extglob         off
~/foobar> ls
abar  afoo  bbar  bfoo
~/foobar> ls !(b*)
-bash: !: event not found
~/foobar> shopt -s extglob  #Enables extglob
~/foobar> ls !(b*)
abar  afoo
~/foobar> ls !(a*)
bbar  bfoo
~/foobar> ls !(*foo)
abar  bbar

Вы можете впоследствии отключить extglob с помощью

shopt -u extglob
  • 11
    Мне нравится эта функция: ls /dir/*/!(base*)
  • 6
    Как включить все ( ), а также исключить! (Б )?
Показать ещё 10 комментариев
181

Параметр extglob shell дает вам более мощное сопоставление шаблонов в командной строке.

Включите его с помощью shopt -s extglob и выключите его с помощью shopt -u extglob.

В вашем примере сначала вы выполните:

$ shopt -s extglob
$ cp !(*Music*) /target_directory

Полные доступные операторы ext end glob bing (выдержка из man bash):

Если опция оболочки extglob включена с использованием встроенного магазина, несколько расширенных распознаются операторы сопоставления шаблонов. В следующем описании, pat tern-list - список одного или нескольких шаблонов, разделенных символом |. Композитные узоры может быть сформирован с использованием одного или нескольких из следующих подматриц:

  • ? (Шаблон)
    Соответствует нулю или одному вхождению данных шаблонов
  • * (шаблон)
    Соответствует нулю или более вхождению данных шаблонов
  • + (шаблон)
    Соответствует одному или нескольким вхождениям данных шаблонов
  • @(шаблон)
    Соответствует одному из заданных шаблонов
  • ! (Шаблон)
    Совпадает со всем, кроме одного из заданных шаблонов

Итак, например, если вы хотите перечислить все файлы в текущем каталоге, которые не являются .c или .h файлами, вы должны:

$ ls -d !(*@(.c|.h))

Конечно, нормальный раскол оболочки работает, поэтому последний пример также можно записать как:

$ ls -d !(*.[ch])
  • 1
    В чем причина -d?
  • 2
    @Koveras для случая, когда один из файлов .c или .h является каталогом.
Показать ещё 1 комментарий
15

Не в bash (что я знаю), но:

cp `ls | grep -v Music` /target_directory

Я знаю, что это не совсем то, что вы искали, но оно решит ваш пример.

  • 0
    По умолчанию ls поместит несколько файлов в строку, что, вероятно, не даст правильных результатов.
  • 10
    Только когда стандартный вывод является терминалом. При использовании в конвейере ls печатает одно имя файла на строку.
Показать ещё 5 комментариев
8

Если вы хотите избежать затрат на использование команды exec, я считаю, что вы можете сделать лучше с помощью xargs. Я думаю, что следующая более эффективная альтернатива

find foo -type f ! -name '*Music*' -exec cp {} bar \; # new proc for each exec



find . -maxdepth 1 -name '*Music*' -prune -o -print0 | xargs -0 -i cp {} dest/
5

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

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
  • 1
    Это делает рекурсивный поиск, который отличается от того, что хочет OP.
  • 1
    использовать -maxdepth 1 для нерекурсивного?
Показать ещё 2 комментария
4

Мои личные предпочтения - использовать команду grep и while. Это позволяет писать мощные, но читаемые сценарии, гарантирующие, что вы в конечном итоге выполняете именно то, что хотите. Кроме того, используя команду эхо-сигнала, вы можете выполнить сухой прогон перед выполнением фактической операции. Например:

ls | grep -v "Music" | while read filename
do
echo $filename
done

распечатает файлы, которые вы в конечном итоге скопируете. Если список верен, следующим шагом будет просто заменить команду echo командой copy следующим образом:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
  • 1
    Это будет работать до тех пор, пока в именах ваших файлов не будет ни табуляции, ни новой строки, ни одного пробела подряд или обратной косой черты. Хотя это патологические случаи, хорошо знать о возможности. В bash вы можете использовать, while IFS='' read -r filename , но тогда новые строки остаются проблемой. В общем случае лучше не использовать ls для перечисления файлов; инструменты типа find гораздо лучше подходят.
  • 0
    Без каких-либо дополнительных инструментов: for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Показать ещё 1 комментарий
4

Одно решение для этого можно найти с помощью find.

$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp {} bar \;
$ ls bar
a.txt

Найти есть довольно много вариантов, вы можете получить довольно конкретную информацию о том, что вы включаете и исключаете.

Изменить: Адам в комментариях отметил, что это рекурсивно. найти варианты mindepth и maxdepth могут быть полезны в управлении этим.

  • 0
    Это делает рекурсивное копирование, которое отличается поведением. Он также порождает новый процесс для каждого файла, который может быть очень неэффективным для большого количества файлов.
  • 0
    Стоимость порождения процесса примерно равна нулю по сравнению со всеми операциями ввода-вывода, которые генерирует копирование каждого файла. Так что я бы сказал, что это достаточно хорошо для случайного использования.
Показать ещё 4 комментария
3

В bash альтернативой shopt -s extglob является GLOBIGNORE variable. Это не лучше, но мне легче запомнить.

Примером может быть то, что хотел исходный плакат:

GLOBIGNORE="*techno*"; cp *Music* /only_good_music/

Когда закончите, unset GLOBIGNORE, чтобы иметь возможность rm *techno* в исходном каталоге.

1

Трюк, который я еще не видел здесь, который не использует extglob, find или grep, должен обрабатывать два списка файлов как наборы и "различать" их с помощью comm:

comm -23 <(ls) <(ls *Music*)

comm предпочтительнее, чем diff, потому что у него нет лишней жесткости.

Это возвращает все элементы набора 1, ls, которые также не входят в набор 2, ls *Music*. Это требует, чтобы оба набора были в порядке сортировки для правильной работы. Нет проблем для ls и расширения glob, но если вы используете что-то вроде find, обязательно вызывайте sort.

comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)

Потенциально полезный.

  • 1
    Одним из преимуществ исключения является не прохождение каталога в первую очередь. Это решение делает два обхода подкаталогов - один с исключением и один без.
  • 0
    Очень хорошая мысль, @MarkStosberg. Хотя одно из преимуществ этой техники - вы можете читать исключения из реального файла, например, comm -23 <(ls) exclude_these.list
1

В следующих работах перечислены все *.txt файлы в текущем каталоге, кроме тех, которые начинаются с числа.

Это работает в bash, dash, zsh и всех других совместимых с POSIX оболочках.

for FILE in /some/dir/*.txt; do    # for each *.txt file
    case "${FILE##*/}" in          #   if file basename...
        [0-9]*) continue ;;        #   starts with digit: skip
    esac
    ## otherwise, do stuff with $FILE here
done
  • В первой строке шаблон /some/dir/*.txt вызовет цикл цикла for для всех файлов в /some/dir, чье имя заканчивается на .txt.

  • Во второй строке аргумент case используется для отсечения нежелательных файлов. - Выражение ${FILE##*/} удаляет любой ведущий компонент имени dir из имени файла (здесь /some/dir/), так что паттерны могут соответствовать только базовому имени файла. (Если вы только отбираете имена файлов на основе суффиксов, вы можете сократить это до $FILE.)

  • В третьей строке все файлы, соответствующие строке case pattern [0-9]*), будут пропущены (оператор continue переходит к следующей итерации цикла for). - Если вы хотите, чтобы вы могли сделать что-то более интересное здесь, например. например, пропустить все файлы, которые не начинаются с буквы (a-z) с помощью [!a-z]*, или вы можете использовать несколько шаблонов для пропуска нескольких типов имен файлов, например. [0-9]*|*.bak пропустить файлы как файлы .bak, так и файлы, которые не начинаются с числа.

  • 0
    Это не работает (Debian). Вы проверяли это?
  • 0
    Doh! Была ошибка (я сопоставлял *.txt вместо * ). Исправлено сейчас.
1

это сделало бы это, исключая точно "Музыка"

cp -a ^'Music' /target

для этого и для исключения таких вещей, как Music? * или *? Music

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target
  • 0
    На странице справочника cp в MacOS есть опция -a но она делает что-то совершенно другое. Какая платформа поддерживает это?

Ещё вопросы

Сообщество Overcoder
Наверх
Меню