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

325

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

cp [exclude-matches] *Music* /target_directory

Что должно пойти вместо [exclude-match] для достижения этой цели?

user4812
источник

Ответы:

375

В 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
Винко Врсалович
источник
14
Мне нравится эта функция:ls /dir/*/!(base*)
Эрик Робертсон
6
Как включить все ( ), а также исключить! (Б )?
Элайджа Линн
4
Как бы вы соответствовали, скажем, всему, начиная с f, кроме foo?
Нолдорин
8
Почему это отключено по умолчанию?
weberc2
3
shopt -o -u histexpand, если вам нужно искать файлы с восклицательными знаками в них - по умолчанию, extglob отключен по умолчанию, чтобы он не мешал histexpand, в документации объясняется, почему это так. сопоставлять все, что начинается с f, кроме foo: f! (oo), конечно, 'food' все равно будет совпадать (вам понадобится f! (oo *), чтобы остановить вещи, начинающиеся с 'foo' или, если вы хотите избавиться некоторые вещи, оканчивающиеся на '.foo', используют! ( .foo) или с префиксом: myprefix! ( .foo) (соответствует myprefixBLAH, но не myprefixBLAH.foo)
osirisgothra
227

Опция extglobоболочки дает вам более мощное сопоставление с образцом в командной строке.

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

В вашем примере вы бы изначально сделали:

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

Полный доступен внутр закончился GLOB операторы Бинг являются (выдержка из man bash):

Если опция оболочки extglob включена с помощью встроенной функции shopt, распознаются несколько расширенных операторов сопоставления с образцом. Список шаблонов - это список из одного или нескольких шаблонов, разделенных знаком |. Составные шаблоны могут быть сформированы с использованием одного или нескольких из следующих подшаблонов:

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

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

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

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

$ ls -d !(*.[ch])
tzot
источник
1
В чем причина -d?
Big McLargeHuge
2
@Koveras для случая, когда один из файлов .cили .hявляется каталогом.
Цот
@DaveKennedy Это список всего в текущем каталоге D, но не содержимое подкаталогов, которые могут содержаться в каталоге D.
Спурра
23

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

cp `ls | grep -v Music` /target_directory

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

ejgottl
источник
По умолчанию ls поместит несколько файлов в строку, что, вероятно, не даст правильных результатов.
Даниэль Бангерт
10
Только когда стандартный вывод является терминалом. При использовании в конвейере ls печатает одно имя файла на строку.
Адам Розенфилд
ls помещает несколько файлов в строку при выводе на терминал. Попробуйте сами - «ls | less» никогда не будет иметь несколько файлов в строке.
SpoonMeiser
3
Он не будет работать с именами файлов, содержащими пробелы (или другие символы белого пространства).
Цот
7

Если вы хотите избежать затрат на использование команды 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/
Стив
источник
6

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

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

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

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

mivk
источник
5

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

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
mipadi
источник
1
Это делает рекурсивный поиск, который отличается от того, что хочет OP.
Адам Розенфилд
1
использовать -maxdepth 1для нерекурсивных?
автомат
Я обнаружил, что это самое чистое решение без необходимости включать / отключать параметры оболочки. В этом посте рекомендуется использовать параметр -maxdepth, чтобы получить результат, необходимый для OP, но все зависит от того, чего вы пытаетесь достичь.
Дэвид Лапойнт
Использование findобратных галочек будет нарушать неприятные способы, если он найдет какие-либо нетривиальные имена файлов.
tripleee
5

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

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гораздо лучше подходят.
Thedward
Без каких-либо дополнительных инструментов:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Thedward
mywiki.wooledge.org/ParsingLs перечисляет ряд дополнительных причин, по которым вам следует избегать этого.
tripleee
5

Уловка, которую я еще не видел здесь, которая не использует 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
Одним из преимуществ исключения является не прохождение каталога в первую очередь. Это решение делает два обхода подкаталогов - один с исключением и один без.
Марк Стосберг
Очень хорошая мысль, @MarkStosberg. Хотя одно из преимуществ этой методики в том, что вы можете читать исключения из фактического файла, напримерcomm -23 <(ls) exclude_these.list
Джеймс М. Лей
3

Одно решение для этого можно найти с помощью 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 могут быть полезны для управления этим.

Даниэль Бангерт
источник
Это делает рекурсивное копирование, которое отличается поведением. Он также порождает новый процесс для каждого файла, который может быть очень неэффективным для большого количества файлов.
Адам Розенфилд
Стоимость порождения процесса примерно равна нулю по сравнению со всеми операциями ввода-вывода, которые генерирует копирование каждого файла. Так что я бы сказал, что это достаточно хорошо для случайного использования.
dland
Некоторые обходные пути для порождения процесса: stackoverflow.com/questions/186099/…
Vinko Vrsalovic
используйте «-maxdepth 1», чтобы избежать рекурсии.
ejgottl
используйте обратные пометки, чтобы получить аналог расширения find -maxdepth 1 -not -name '*Music*'подстановочного
знака
2

В следующих работах перечислены все *.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
  1. В первой строке шаблон /some/dir/*.txtзаставит forцикл перебирать все файлы /some/dir, имена которых заканчиваются на .txt.

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

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

зрайм
источник
Doh! Был баг (я сопоставил *.txtвместо просто *). Исправлено сейчас.
Зрайм
0

это сделало бы это, исключая именно «музыку»

cp -a ^'Music' /target

это и это для исключения таких вещей, как музыка? * или *? музыка

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