Получить имя файла без расширения в Bash

у меня есть следующие for цикл индивидуально sort все текстовые файлы внутри папки (т. е. создание отсортированного выходного файла для каждого).

for file in *.txt; 
do
   printf 'Processing %sn' "$file"
   LC_ALL=C sort -u "$file" > "./${file}_sorted"  
done

это почти идеально, за исключением того, что он в настоящее время выводит файлы в формате:

originalfile.txt_sorted

...тогда как хотелось бы выводить файлы в формате:

originalfile_sorted.txt 

это так ${file} переменная содержит имя файла с расширением. Я запускаю Cygwin на вершине Windows. Я не уверен, как это будет вести себя в истинной среде Linux, но в Windows это смещение расширения делает файл недоступным для Проводника Windows.

как я могу отделить имя файла от расширения, так что я могу добавить _sorted суффикс между ними, что позволяет мне легко различать оригинальные и сортированные версии файлов, сохраняя при этом расширения файлов Windows нетронутыми?

Я смотрел на то, что может be возможно решения, но мне они кажутся более приспособленными для решения более сложных проблем. Что еще важнее, с моим нынешним bash знание, они идут над моей головой, так что я держу надежду, что есть более простое решение, которое относится к моей скромной for цикл, или же кто-то может объяснить, как применить эти решения к моей ситуации.

14
задан Hashim
02.03.2023 22:47 Количество просмотров материала 3444
Распечатать страницу

1 ответ

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

эта твоя линия

for file in *.txt

указывает, что расширение известно заранее (Примечание: POSIX-совместимые среды чувствительны к регистру, *.txt не соответствует FOO.TXT). В таком случае

basename -s .txt "$file"

должен возвращать имя без расширения (basename также удаляет путь к каталогу:/directory/path/filenamefilename; в вашем случае это не имеет значения, потому что $file не содержит такого пути). Для использования инструмента в коде необходима подстановка команд, которая выглядит примерно так:$(some_command). Команда подстановки принимает вывод some_command, обрабатывает его как строку и помещает в $(…) есть. Ваше конкретное перенаправление будет

… > "./$(basename -s .txt "$file")_sorted.txt"
#      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the output of basename will replace this

вложенные кавычки здесь в порядке, потому что Bash достаточно умен, чтобы знать кавычки в $(…) парные вместе.

это можно улучшить. Примечание basename - это отдельный исполняемый файл, а не встроенная оболочка (в bash run type basename для всех type cd). Нерест любой лишний процесс обходится дорого, требует ресурсов и времени. Нерест его в петле обычно выполняет плохо. Поэтому вы должны использовать все, что предлагает вам оболочка, чтобы избежать дополнительных процессов. В этом случае решение:

… > "./${file%.txt}_sorted.txt"

синтаксис описан ниже для более общего случай.


в случае, если вы не знаете расширение:

… > "./${file%.*}_sorted.${file##*.}"

синтаксис объяснил:

  • ${file#*.} -$file, но самая короткая строка, соответствующая *. удаляется спереди;
  • ${file##*.} -$file, но самая длинная строка, соответствующая *. удаляется спереди; используйте его, чтобы получить только расширение;
  • ${file%.*} -$file, но самая короткая строка, соответствующая .* удаляется из конец; используйте его, чтобы получить все, кроме расширения;
  • ${file%%.*} -$file, но с самой длинной строкой, соответствующей .* удаляется с конца;

сопоставление шаблонов похоже на glob, а не на regex. Это значит * - подстановочный знак для нуля или более символов, ? - это замена одного персонажа (нам не нужны ? в вашем случае, хотя). Когда вы вызываете ls *.txt или for file in *.txt; вы используете тот же шаблон, соответствующий механизм. Узор без подстановочных знаков допускается. Мы уже использовали ${file%.txt} здесь .txt - это шаблон.

пример:

$ file=name.name2.name3.ext
$ echo "${file#*.}"
name2.name3.ext
$ echo "${file##*.}"
ext
$ echo "${file%.*}"
name.name2.name3
$ echo "${file%%.*}"
name

но будьте осторожны:

$ file=extensionless
$ echo "${file#*.}"
extensionless
$ echo "${file##*.}"
extensionless
$ echo "${file%.*}"
extensionless
$ echo "${file%%.*}"
extensionless

по этой причине следующая штуковина может быть полезным (но это не так, объяснение ниже):

${file#${file%.*}}

он работает, идентифицируя все, кроме расширения (${file%.*}), затем удаляет это из всей строки. Результаты выглядят так:

$ file=name.name2.name3.ext
$ echo "${file#${file%.*}}"
.ext
$ file=extensionless
$ echo "${file#${file%.*}}"

$   # empty output above

Примечание. the . включен в это время. Вы можете получить неожиданные результаты, если $file содержит литерал * или ?; но Windows (где расширения имеют значение) не дает эти символы в именах файлов в любом случае, так что вам может быть все равно. Однако […] или {…}, если присутствует, может вызвать их собственную схему сопоставления с образцом и сломать решение!

ваше" улучшенное " перенаправление будет:

… > "./${file%.*}_sorted${file#${file%.*}}"

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

действительно улучшенное перенаправление:

… > "./${file%.*}_sorted${file#"${file%.*}"}"

двойное цитирование делает ${file%.*} не действовать как шаблон! Bash достаточно умен, чтобы различать внутренние и внешние кавычки, потому что внутренние встроены во внешнюю ${…} синтаксис. я думаю, что это правильно путь.

еще одно (несовершенное) решение, давайте проанализируем его по образовательным причинам:

${file/./_sorted.}

заменяет первый . с _sorted.. Он будет работать нормально, если у вас есть не более одной точки в $file. Похожий синтаксис ${file//./_sorted.}, который заменяет все точки. Насколько я знаю, нет никакого варианта, чтобы заменить последние точка только.

все еще первоначальное решение для файлов с . выглядит надежной. Решение для без расширений $file тривиально: ${file}_sorted. Теперь все, что нам нужно, это способ разделить два дела. Вот это:

[[ "$file" == *?.* ]]

возвращает статус выхода 0 (true) тогда и только тогда, когда содержимое $file переменная соответствует шаблону правой стороны. Шаблон говорит: "есть точка после хотя бы одного символа" или, что то же самое, "есть точка, которая не находится в начале". Дело в том, чтобы лечить скрытые файлы Linux (например,.bashrc), а без расширений, если есть другое точка где-то.

Примечание нам нужно [[ здесь, а не [. Первый более мощный, но, к сожалению,не портативный; последнее является портативным, но слишком ограниченным для нас.

логика теперь выглядит так:

[[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"

после этого, $file1 содержит желаемое имя, поэтому перенаправление должно быть

… > "./$file1"

и весь код (*.txt заменить *, чтобы указать нам работа с любым расширением или без расширения):

for file in *; 
do
   printf 'Processing %s\n' "$file"
   [[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"
   LC_ALL=C sort -u "$file" > "./$file1"  
done

это также попытается обработать каталоги (если они есть); вы уже знаете что делать исправить.

19
отвечен Kamil Maciorowski 2023-03-04 06:35

Постоянная ссылка на данную страницу: [ Скопировать ссылку | Сгенерировать QR-код ]

Ваш ответ

Опубликуйте как Гость или авторизуйтесь

Имя

Похожие вопросы про тегам:

bash
bash-scripting
cygwin
filenames
windows
Вверх