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

3050
Hashim

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

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

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

originalfile.txt_sorted 

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

originalfile_sorted.txt 

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

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

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

6

1 ответ на вопрос

19
Kamil Maciorowski

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

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

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 достаточно умен, чтобы знать, что кавычки внутри $(…)объединены в пару.

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

… > "./$_sorted.txt" 

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


Если вы не знаете расширение:

… > "./$_sorted.$" 

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

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

Сопоставление с образцом похоже на глобус, а не на регулярное выражение. Это означает *, что подстановочный знак для нуля или более символов, ?это подстановочный знак для ровно одного символа ( ?хотя в нашем случае мы не нуждаемся ). Когда вы вызываете ls *.txtили for file in *.txt;используете тот же механизм сопоставления с образцом. Шаблон без подстановочных знаков допускается. Мы уже использовали $где .txtшаблон.

Пример:

$ file=name.name2.name3.ext $ echo "$" name2.name3.ext $ echo "$" ext $ echo "$" name.name2.name3 $ echo "$" name 

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

$ file=extensionless $ echo "$" extensionless $ echo "$" extensionless $ echo "$" extensionless $ echo "$" extensionless 

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

$} 

Он работает, идентифицируя все, кроме extension ( $), а затем удаляет это из всей строки. Результаты таковы:

$ file=name.name2.name3.ext $ echo "$}" .ext $ file=extensionless $ echo "$}"  $ # empty output above 

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

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

… > "./$_sorted$}" 

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

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

… > "./$_sorted$"}" 

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

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

$ 

Он заменяет первый .на _sorted.. Это будет хорошо работать, если у вас есть не более одной точки $file. Существует аналогичный синтаксис, $который заменяет все точки. Насколько я знаю, нет варианта заменить только последнюю точку.

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

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

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

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

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

[[ "$file" == *?.* ]] && file1="./$_sorted.$" || file1="$_sorted" 

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

… > "./$file1" 

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

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

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

Once again, a brilliant answer, thank you. I'm definitely a long way from understanding all of it, but for now I'm gonna leave that to one side and just read up more on command substitution when I do have the time. One question I do have: you mentioned that `… > "./$_sorted.txt"` "avoids extra processes" - is this because we're using basename in the `$file` variable outside of the `for` loop here: `basename -s .txt "$file"`... or have I misunderstood? Hashim 5 лет назад 0
@Hashim `… > "./$_sorted.txt"` is the only change you need to do to your script (ellipsis `…` just indicates everything you have before `>`, it's *not* an actual character you should place in your script; replace `>` and the rest of the line with `> "./$_sorted.txt"`). It avoids extra processes because now we don't use `basename` *at all*; the whole magic is done by the shell itself thanks to `$` syntax. Side note: sole `basename -s .txt "$file"` just prints something; if you think it alters the variable, you're wrong. Kamil Maciorowski 5 лет назад 0
Ах, значит подстановка команд используется вместо `basename`, а не рядом с ней. Понимаю. В очередной раз благодарим за помощь. Hashim 5 лет назад 0
@Hashim Not quite. This fragment `> "./$(basename -s .txt "$file")_sorted.txt"` uses command substitution, the command is `basename …`. You either use this or `> "./$_sorted.txt"` which doesn't use command substitution. So it's (command substitution + `basename`) *xor* just fancy variable expansion `$` without command substitution. Kamil Maciorowski 5 лет назад 1
@Hashim Или, может быть, я не понял твоего "вместо` basename` ". Kamil Maciorowski 5 лет назад 0
Ах я вижу. Похоже, мне нужно искать расширение переменных в этом случае, ха-ха. В любом случае я применил метод подстановки команд / basename для моего цикла `for`, и я также заметил, что в его работе есть небольшая странность ... Hashim 5 лет назад 0
Если исходное имя файла (не включая расширение в нем) содержит квадратные скобки с даже одним (обычным) символом в них, например `[i]`, имя выходного файла превращается в `originalfile_sortedoriginalfile.txt` - в другими словами, он добавляет исходное имя файла к новому имени файла * снова *, когда это не должно быть. Квадратные скобки, содержащие не менее 1 символа, являются единственной причиной этого; Скобки, фигурные скобки и одинарные или пустые квадратные скобки не вызывают этой проблемы. Hashim 5 лет назад 0
Давайте [продолжим это обсуждение в чате] (https://chat.stackexchange.com/rooms/83190/discussion-between-kamil-maciorowski-and-hashim). Kamil Maciorowski 5 лет назад 0
Thanks for the extra efforts after I brought the square bracket problem to your attention. To clarify now: `file1="./$_sorted.$"` supports only extensions with one period, so the final solution as a whole shouldn't be used on files that have more than one period in them? I don't have a need to do so, I just want to make sure this is clear as it doesn't seem to be so in the current answer and I want to ensure I don't misuse this loop in the future. Hashim 5 лет назад 0
@ Хашим Более одной точки не проблема с этим. Ноль точек есть. Kamil Maciorowski 5 лет назад 0
Ах, теперь я понимаю, что в ответе так много фрагментов кода, что он запутался. Это последний фрагмент кода, с которым я остался: https://pastebin.com/6XvWdcKB. Он отлично работает с моими текущими данными, но если вы не возражаете, я был бы признателен, если бы вы просмотрели его, просто чтобы выяснить, чего мне не хватает. В частности, я подумал, что может потребоваться оператор `if`, содержащий все и прерванный в случае сбоя` [-f "$ file"] `. Hashim 5 лет назад 0
@Hashim Я улучшил свой ответ, добавил относительно простое, но надежное решение. Найдите его там, где выделен жирный текст (или воспользуйтесь [историей изменений] (https://superuser.com/posts/1358024/revisions)). Kamil Maciorowski 5 лет назад 0