Как я могу получить первое совпадение из подстановочного знака?

39

Такие оболочки, как Bash и Zsh, расширяют подстановочный знак в аргументы, столько аргументов, сколько соответствует шаблону:

$ echo *.txt
1.txt 2.txt 3.txt

Но что, если я хочу, чтобы был возвращен только первый матч, а не все?

$ echo *.txt
1.txt

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

Флимм
источник
ls * .txt | голова -1?
Архемар
1
@Archemar: не работает с символами новой строки в именах файлов.
Flimm

Ответы:

26

Один надежный способ в bash - развернуть в массив и вывести только первый элемент:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(Вы можете даже просто echo $files , отсутствующий индекс обрабатывается как [0].)

Это безопасно обрабатывает пробел / tab / newline и другие метасимволы при расширении имен файлов. Обратите внимание, что действующие настройки локали могут изменить то, что «первое».

Вы также можете сделать это интерактивно с помощью функции завершения bash :

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo

Это связывает _echoфункцию для завершения аргументовecho команды (переопределяя нормальное завершение). В приведенном выше коде добавлено дополнительное «*», вы можете просто нажать «tab» на частичном имени файла, и, надеюсь, правильная вещь произойдет.

Код немного запутанным, а не набор или предположить nullglob( shopt -s nullglob) мы проверяем , compgen -Gможно расширить Glob в некоторых матчах, то мы расширим благополучно в массив, и , наконец , установить COMPREPLY так , чтобы процитировать надежен.

Вы можете частично сделать это (программно расширить глобус) с помощью bash compgen -G, но это не надежно, так как выводит без кавычек в стандартный вывод.

Как обычно, завершение довольно чревато, это нарушает завершение других вещей, включая переменные среды (см. _bash_def_completion()Функцию здесь для деталей эмулирует поведение по умолчанию).

Вы также можете просто использовать compgenвне функции завершения:

files=( $(compgen -W "$pattern") )

Следует отметить, что «~» - это не глобус, он обрабатывается bash на отдельной стадии раскрытия, как и переменные $ и другие раскрытия. compgen -Gпросто выполняет глобализацию имени файла, но compgen -Wдает вам все расширения bash по умолчанию, хотя, возможно, слишком много расширений (включая ``и $()). В отличие от -G, -W это безопасно цитируется (я не могу объяснить несоответствие). Поскольку цель -Wсостоит в том, что он расширяет токены, это означает, что он расширит «a» до «a», даже если такого файла не существует, поэтому, возможно, он не идеален.

Это легче понять, но может иметь нежелательные побочные эффекты:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Затем:

touch $'curious \n filename'

echo curious*tab

Обратите внимание на использование printf %qдля безопасного цитирования значений.

Последний вариант - использовать вывод с разделителями 0 для утилит GNU (см. Часто задаваемые вопросы по bash ):

pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )

Эта опция дает вам немного больше контроля над порядком сортировки (порядок при развертывании глоба будет зависеть от вашей локали / LC_COLLATEи может или не может сложить регистр), но в остальном довольно большой молоток для такой маленькой проблемы ;-)

mr.spuratic
источник
21

В zsh используйте [1] спецификатор glob . Обратите внимание, что хотя этот особый случай возвращает не более одного совпадения, он все равно остается списком, и глобусы не раскрываются в контекстах, которые ожидают одно слово, например, присваивания (кроме присвоений массива).

echo *.txt([1])

В ksh или bash вы можете поместить весь список совпадений в массив и использовать первый элемент.

tmp=(*.txt)
echo "${tmp[0]}"

В любой оболочке вы можете установить позиционные параметры и использовать первый.

set -- *.txt
echo "$1"

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

echo "$(set -- *.txt; echo "$1")"

Вы также можете использовать функцию, которая имеет собственный набор позиционных параметров.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
Жиль "ТАК - прекрати быть злым"
источник
1
И чтобы получить первые $ n $ совпадения, вы можете использовать*.txt([1,n])
Emre
6

Пытаться:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

Обратите внимание, что расширение имени файла сортируется в соответствии с последовательностью сортировки, действующей в текущей локали.

cuonglm
источник
3

Простое решение:

sh -c 'echo "$1"' sh *.txt

Или используйте, printfесли хотите.

G-Man говорит: «Восстанови Монику»
источник
1

Я просто наткнулся на этот старый вопрос, удивляясь тому же. Я закончил с этим:

echo $(ls *.txt | head -n1)

Можно, конечно, заменить headс tailи -n1с любым другим номером.


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

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Не работает на BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (Работает с любым специальным персонажем)
user149485
источник
3
Нет, это не удастся, если в имени файла есть новые строки.
Исаак
7
В каком сумасшедшем мире мы живем, где в именах файлов есть новые строки?
Биллиноа
-1

Случай использования, с которым я часто сталкиваюсь, определяет либо верхний / нижний каталог после расширения glob (например, каталог, полный версионных SDK или инструментов сборки). В этой ситуации я обычно хочу сохранить это имя каталога в переменной для использования в нескольких местах внутри сценария оболочки.

Эта команда обычно делает это для меня:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

Отказ от ответственности: расширение Glob не будет сортировать ваши папки по semver; Вы были предупреждены. Это замечательно, если у вас есть Dockerfileтолько одна версия каталога, но эта версия каталога может отличаться от изображения к изображению 🤷

Эндрю Одри
источник
Добро пожаловать в U & L! Это обрабатывает большинство имен каталогов, но не обрабатывает имена каталогов с новой строки. Попробуйте создать такой каталог mkdir "$(echo one; echo two)"и посмотрите, что я имею в виду.
Flimm
В чем преимущество по сравнению с другими альтернативами, особенно используемой версией tail?
Ральф Фридл
Стандарт dirnameберет только одно имя пути, поэтому вы не можете полагаться на то, что он работает с несколькими путями, если вы не знаете, что ваша конкретная реализация поддерживает его.
Кусалананда
@Flimm Хороший вопрос; Я думаю, что у большинства разработчиков возникли бы более серьезные проблемы, если бы в структуре их папок были новые строки ... Мне никогда не приходилось сталкиваться с этим, и я не ожидаю, что с какими-либо полуприличными контейнерами и программным обеспечением я использую
Андрей Одри
@RalfFriedl Хороший вопрос; это, по сути, отфильтровывает все, что не является допустимым каталогом (и не будет перечислять / перемещать. и ..)
Эндрю Одри