6

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

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

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

originalfile.txt_sorted

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

originalfile_sorted.txt 

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

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

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

1 ответ1

19

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

Эта ваша линия

for file in *.txt

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

basename -s .txt "$file"

должен вернуть имя без расширения (basename также удаляет путь к каталогу: /directory/path/filename & rightarrow; filename ; в вашем случае это не имеет значения, поскольку $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 , но с самой длинной совпадающей строкой .* удаляется с конца;

Сопоставление с образцом похоже на глобус, а не на регулярное выражение. Это означает, что * подстановочный знак для нуля или более символов ? подстановочный знак только для одного символа (нам не нужно ? хотя в твоем случае). Когда вы вызываете 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

Обратите внимание . включен в этот раз. Вы можете получить неожиданные результаты, если $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

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

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .