С этим вопросом обратился мой давний товарищ Денис. Ранее в блоге я показывал массовое переименование файлов методом поиск/замена и добавлением счетчика к файлам. Приклеивание имени родительской папки выглядит менее распространенной задачей. Тем не менее, я много раз видел в форуме скриптов вопросы про это (фильтр также выводит похожие темы). Заметьте, что там всегда хотят батники. А тут сразу PowerShell.
Когда-то я ковырялся в путях оператором -Split. Похожий метод .NET split() фигурировал в блоге с целью парсинга ссылок – для выборки уникальных ссылок на веб-странице и для загрузки с youtube-dl. Имя родительской папки можно получить так:
("C:\temp\f.txt" -Split "\\")[-2]
В кавычках указан разделитель \
(второй обратный слэш там нужен для экранирования). Получается массив из отрезков пути, где [-2]
дает второй элемент с конца. Это как раз имя родительской папки.
Однако Василий Гусев давно повелел освоить командлет Split-Path. Он функциональный, конечно, но в рамках данной задачи интуитивностью не блещет. Судите сами:
Split-Path (Split-Path -Path "C:\temp\f.txt" -Parent) -Leaf
Второй командлет (в скобках) выдает путь к родительской папке с помощью параметра -Parent
. А первый Split-Path
с параметром -Leaf
извлекает из пути последний отрезок, т.е. искомое имя папки.
Дальше все сводится к ванлайнеру, но так его будет чуть легче воспринимать:
Get-ChildItem -File | ForEach-Object { Rename-Item $_.FullName -NewName ("$(Split-Path (Split-Path $_.FullName -Parent) -Leaf)_$($_.Basename)$($_.Extension)") -WhatIf }
Новое имя собирается из кирпичиков:
$(Split-Path (Split-Path $_.FullName -Parent) -Leaf)
— имя родительской папки_
— просто разделитель между именем родительской папки и текущим именем файла$($_.Basename)
— текущее имя файла без расширения$($_.Extension)
— расширение
Заметьте, что обрабатываются файлы только в текущей папке. Добавьте к Get-ChildItem параметр -Recurse
, если для вложенных папок необходимо добавлять их имена к файлам. Если же для файлов из вложенных папок нужно добавлять имя «главной» родительской, проще поместить его в переменную. И дальше подставлять ее вместо двойного Split-Path для каждого файла.
А о чем был ваш последний скрипт или хотя бы ванлайнер?
Евгений Грицан
Ха, практически то же самое — скачал сезон сериала, в котором субтитры лежали в отдельных папках, у которых уже имя совпадало с именем эпизода. Вот и переименовывал файлы субтитров в имя родительской папки и складывал их в корень, чтобы плеер автоматически их подхватывал при воспроизведении. На питоне строк 5 накидал.
Vadim Sterkin
Да, задача более популярна, чем кажется:)
Матвей Солодовников
Вадим, спасибо, очень полезные сведения! Узнал впервые про знак «минус» у оператора Split — круто!
Все свои скрипты давно переписываю на PowerShell (и рабочие, и для домашних нужд) — по работе это нужно для безопасности (скрипты подписываются, неподписанные просто не запустятся).
Последний скрипт, который я правил, занимается автоматической рассылкой писем всем тем пользователям домена, у которых паролю осталось жить меньше двух недель.
Oneliner был на тему поиска компьютера, на котором залогинился пользователь (при каждом логоне пользователь обновляет значения кастомных атрибутов компьютера, на котором он вошёл):
Vadim Sterkin
Матвей, тут минус не про оператора Split все-таки. Это свойство массивов, т.е. нумерация с конца массива.
По поводу истечения паролей наш ИТ-отдел присылает забавные письма, вроде «Ваш пароль истекает через 3.14 дней» :)
Виктор Соломонович Радчин
Как вариант. Имя родительской папки можно получить без иcпользования оператора -Split просто читая свойства возвращаемых командлетами Get-ChildItem и Get-Item объектов:
Lecron
Моё последнее вряд ли будет интересно. Да и не вписывается пока PS в промежуток между bat и python. А вот интересный вопрос возник. Есть ли возможность многопоточной обработки? Какой нибудь ForEach-Object-ThreadPool. Например получили список файлов и в 4 потока стали их обрабатывать какой-то внешней программой. Пока использую для этого программу rush, но уж очень много ограничений и синтаксис иногда зубодробительный.
Vadim Sterkin
Jobs? https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_jobs
Lecron
ForEach-Object -Parallel -ThrottleLimit N
Спасибо. Буду пробовать.
Видно гуглил это вопрос до выхода PS7 и предлагалась лишь обвязка на пару десятков строк (возможно на тех же Jobs), чтобы решить простейший по нынешним временам вопрос. Тогда посчитал это нерациональным.
Vadim Sterkin
Да, в 5.1 -Parallel нет, да и вообще 5.1 зависла в ОС. Проще 7.х из магазина поставить, хотя есть нюансы.
Иначе MSI, и есть вариант обновлять через WU полуавтоматически https://t.me/sterkin_ru/1251
Виктор Соломонович Радчин
Или даже ещё короче:
Vadim Sterkin
И как выглядит решение задачи в итоге?
Виктор Соломонович Радчин
Да, собственно как и у Вас. Примерно также:
Vadim Sterkin
Спасибо, зачёт!:)
Itigo Kurosaki
А в целом. Лучше осветите тему параллельного выполнения команд в powershell. Ибо никто почти в этом не понимает и не пишет про это.
artem
В подобных случаях (т.е., в 99% случаев, когда тебе не надо разворачивать переменные в строке) вместо двойных кавычек следует использовать одинарные. Как правило, это как раз спасает от необходимости что-то экранировать.
1.
но почему-то для -split это не работает.
должно бы работать, но нет.
2.
работает, но всё ещё приходится экранировать. Т.е., польза от одинарных кавычек в данном случае неочевидна.
(Хотя это всё равно лучше, чем двойные, просто из соображений выработки хороших привычек.)
3.
но! Если заменить -split (оператор PowerShell) на .split() (метод .NET), то всё работает. И с одинарными кавычками, и без экранирования.
4.
хотя, раз уж мы дошли до методов .NET, то ещё правильнее было бы так.
(потому что на разных платформах символ-разделитель может быть другим. Вдруг тебя читают люди с macOS?)
5.
ну и для полноты картины, если ты всё-таки хочешь использовать -split и надо экранировать. Для этого тоже есть свой метод.
понятно, что для текущей задачи это несколько перебор. Так что из соображений наглядности считаю самым оптимальным вариант 3.
а вот это, по-моему, классический случай, когда разбиение на пару строк увеличит читаемость на порядок.
ну и конечный вариант будет выглядеть как-то так.
кстати, зачем в твоём примере ты используешь отдельно .BaseName и .Extension? В этом был бы смысл, если бы тебе надо что-то вставлять между ними. Но если ты их всё равно используешь подряд, как в оригинале, то проще использовать .Name.
ну и кавычек больше никаких нет.
Да, и кстати. Если ты не используешь -Recurse, т.е. все файлы заведомо будут в одной папке, то «сложный» код определения имени папки можно вообще вынести за скобки. Тогда будет чуть эффективнее (кто тут хотел многопоточной обработки) и сильно нагляднее, как мне кажется.
Но менее гибко, конечно, потому что если вдруг кто-то захочет рекурсивной обработки, то половину придётся переписывать :)
Lecron
Наткнулся еще на конвейерный синтаксис
Кмк, читается лучше чем вложенный из статьи или с временной переменной у вас.
Vadim Sterkin
Угу, но метод дотнет фигурировал уже в двух статьях блога (и я сослался на них в тексте), а теперь оператор для разнообразия. Но вообще пост про командлет, который ранее не освещался в блоге — Split-Path. Как раз на случай
Скорее с Linux втч с WSL.
На случай, если кто-то захочет имя папки впихнуть в имя файла не в начале, а в конце имени. Это не расписано в тексте, но у себя я бы пожалуй сделал именно так, чтобы легче читались исходные имена файлов.
Возможно. А возможно Поданс скажет, что я устроил чемпионат мира по созданию переменных :)
artem
дело вкуса, конечно. Но, на мой взгляд, — конвейер хорошо работает с точки зрения читаемости только в тех случаях, когда суть объекта, передающегося по конвейеру, очевидна. Как в моём недавнем примере про Get-Disk | Get-Partition | Get-Volume. Здесь, просто по названию команды, на каждом этапе можно достаточно легко предположить, какой объект получается на выходе одного коммандлета и что попадает на вход следующего, поэтому наглядность не страдает.
В случае с повторяющимися Split-Path это уже не настолько очевидно. Поэтому я люблю промежуточные переменные — не только для того, чтобы красиво разбивать код на несколько строк, но и для того, чтобы использовать их названия для пояснения, что делает код на каждом этапе.
***
Кроме того, у конвейера есть и ещё один дидактический недостаток. Читателю сложнее выполнить только часть кода и исследовать результат конкретной команды. А я, например, очень люблю так поступать с примерами, которые не очень понятны сходу, или которые используют незнакомые коммандлеты.
Т.е., можно, конечно, скопировать часть строки до символа конвейра, потом руками дописать в начало присвоение переменной, и таким образом получить объект для изучения. Но это дополнительные телодвижения, которые отвлекают от сути и быстро становятся утомительными. А если сам пример изначально разбит на этапы с промежуточными переменными, изучение такого кода становится гораздо проще и приятнее.