Что надо было сделать
Однажды мне понадобилось объединить пачку текстовых файлов, лежащих в одной директории, в один файл. Делать руками этого не хотелось и мне на помощь, как всегда, пришёл Гугл! Я много слышал о мощности такого средства, как PowerShell, и решил использовать его для этой "мегазадачи". Хотя бы потому, что с убогостью средства cmd.exe я знаком не по наслышке. Ну а делать это руками — не наш путь.
Что пошло не так
Гугл подсказал мне, что сделать это можно простой командой
Get-ChildItem -Filter *.log | Get-Content | Out-File result.txt
"Действительно круто! Просто Unix-way какой-то!" — подумал я. Скопировал команду, слегка модифицировал её и нажал Enter. Курсор перешёл на новую строку… и больше ничего. В файловом менеджере я открыл итоговый файл — в нём действительно было что-то похожее на нужный результат. В нём было много строк из исходных файлов. Вернувшись в консоль я увидел, что процесс всё ещё… в процессе. Помог Ctrl+C.
Присмотревшись к размеру файла я увидел, что он как-то подозрительно велик. ЕГо размер превышел 100 Мегабайт. Хотя водные данные не были такими большими.
Почему это случилось?
Всё дело в моей "лёгкой модификации". Мне просто не нужен был фильтр по расширению. Да и параметр этот не является обязательным. И получилось, что команда создала результирующий файл, увидела, что он есть в директории, прочитала его и снова записала своё содержимое в конец и делала это, пока я не нажал Ctrl+C Никак по другому непрерывный рост вызодного файла я объянить для себя не смог
Я повторил это в "стерильных" условиях. Для простоты и чистоты эксперимента делаю в отдельной директории, так как боюсь убить рабочую машину
Создаю текстовый файл
echo "Hello world" > hello.txt
Выполняю команду
Get-ChildItem | Get-Content | Out-File result.txt
или в короткой форме
dir | cat | Out-File result.txt
Проблема повторяется. Результирующий файл растёт, пополняясь строкой из исходного (или строками из самого себя?). За 10 секунд выполнения:
- одна строка исходного файла превращается в 400 тысяч строк
- размер файла вырос с 11 байт до почти 8 мегабайт
- процессор грузится примерно на 20-25 %.
- перегрузок дисковой подсистемы или оперативной памяти при этом нет. Видимо, PowerShell хорошо оптимизирован в части работы с этими компонентами. )
Так же интересно — если в качестве параметра последней команде указать имя единственного файла в директории, то, конечно же, как вы уже догадались барабанная дробь… в файл запишется пустота!
Вот такая вот "интересная" логика работы
Что получилось
Созданный на первом шаге файл начинает расти. Это поведение как минимум непредсказуемое.
Так же удивило, что операционная система продолжает нормально работать. Файл медленно (или не очень?) растёт, не блокируя работу пользователя.
Чем опасно
Незаметное заполнение дискового пространства.
Как избежать
Фильтровать список входных файлов:
Get-ChildItem -Filter *.log | Get-Content | Out-File result.txt
Но и это не спасёт, если и входные и выходной файл у вас подходят под условие фильтра
P.S.
Я использую версию PowerShell 5.1.17134.407. Кстати, в попытках узнать я испробовл все известные мне способы/логику и здравый смысл (а именно флаги типа **-Version --version -v -h***). Но это не помогло. Выручил, как всегда, Stackoverflow. Вот как можно узнать версию PowerShell
$PSVersionTable.PSVersion
Этот ответ собрал почти 3000 "лайков"! Это конечно меньше, чем ответ на вопрос как закрыть vim, но тоже считаю показательно!
А вообще PowerShell действительно мощная штука (хотя бы в сравнении с cmd.exe) и я, конечно, буду продолжать ею пользоваться!
Комментарии (20)
dipsy
20.12.2018 19:31+1Убогость cmd.exe конечно поражает, приходится аж что-то вот такое писать:
for %f in (*.txt) do type "%f" >> output.txt
Причем оно даже результат к самому себе не дописывает в цикле, а вдруг нам именно это и нужно было
BkmzSpb
20.12.2018 20:37Не спец по
PS
, но полагаю, что идея в ленивом исполнении. Т.е. сначала команда извлекает первый файл, затем — его содержимое, возможно — одну строку, затем дописывает строки в конец выходного файла, пока первый файл не закончится. Потом в какой-то момент доходит до вашего выходного файла и лениво начинает тянуть из него строки, попутно их записывая. Очевидно, строки не заканчиваются. 25% загрузки CPU говорят о том, что у вас скорее всего двухъядерный лэптоп с hyperthreading enabled.
Я попробовал поиграть с командами, решение (вроде как) оказалось простым:
> echo "Hello, World 1!" > test1.txt > echo "Hello, World 2!" > test2.txt > (ls | cat) > out.txt > ls Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2018-12-19 20:36 70 out.txt -a---- 2018-12-19 20:36 36 test1.txt -a---- 2018-12-19 20:36 36 test2.txt > cat out.txt Hello, World 1! Hello, World 2!
Попробуйте — вдруг будет работать.
UPD Здесь до меня по сути то же самое написали.
mayorovp
20.12.2018 09:20Ваше
(ls | cat) > out.txt
— это ровно то же самое, что и авторскоеget-childitem | get-content | out-file result.txt
, только выходной файл называется по-другому.
Вам просто повезло, что имя вашего файла оказалось лексикографически меньше всех остальных файлов в директории, а потому powershell его прочитал самым первым, когда он еще был пустым.
mayorovp
20.12.2018 14:49UPD: почему-то я первый раз не обратил внимания на скобки, даже когда скопировал их. Да, со скобками работать будет, скобки разрывают конвейер и материализуют последовательность.
Но надо понимать, что эта команда загрузит содержимое всех файлов в память.
Khabarovchanin
20.12.2018 21:33убогостью средства cmd.exe
Хм…
for %R in (*.txt) do type %R >> result.txt
dipsy
20.12.2018 05:24А если эту строку вписать в файл merge.cmd и запустить, сработает? У меня почему-то только в консоли cmd такая строка работает, а если батником запускать, находящимся в папке с требуемыми файлами, никакого эффекта.
remzalp
20.12.2018 07:54в батнике надо % удваивать:
for %%R in (*.txt) do @type %%R>> rezult.tx
ну и плюс дурная идея в том же каталоге создавать txt файл, который тоже считается и допишется. Это к удвоению данных :)
mistergrim
20.12.2018 01:50copy *.txt result.txt
dipsy
20.12.2018 05:31Проверил, 3 текстовых файла в папке было, объединились, но результат получился на 1 байт больше суммы исходных, в конце файла символ с кодом 0x1A зачем-то дописался…
mistergrim
20.12.2018 08:190x1A — это EOF. Legacy, ещё из DOS тянется, что поделать.
Можно склеивать так — все символы 0x1a будут вырезаны, новые добавлены не будут:
copy *.txt result.txt /b
evr1ka
20.12.2018 11:21х его з, но изначальная строчка будет работать нормально как задумано:
Get-ChildItem -Filter *.log | Get-Content | Out-File result.txt
Вот если бы result.txt был result.log, тогда да, еще возможна такая ситуация. Не знаю почему вам не был очевиден такой момент. Он же будет по конвееру передавать все по одному элементу, попадающему в шаблон, поэтому да, вечный loop. Можно еще так:
dir | cat > result.txt
neitri
20.12.2018 14:52Get-ChildItem -filter *.log | Where-Object Name -notlike 'result.log'| Get-Content |Out-File result.log
YMax
21.12.2018 07:37А зачем писать итоговый файл в одну папку с исходными и создавать проблему на пустом месте?
solo12zw74 Автор
21.12.2018 09:38Статья больше о том, что это поведение ненормальное и непредсказуемое. Путей решения есть много, если знаешь, что проблема есть.
Вспомнился анекдот:
Приходит мужик к врачу и говорит:
— Доктор, я когда рукой двигаю, у меня вот тут болит
— А когда не двигаете — не болит?
— Нет.
— Ну так просто не двигайте рукой!gecube
21.12.2018 21:12Насчёт — ненормальное и непредсказуемое — Вы погорячились. Просто, видимо, опыта мало.
Бомбануть может где угодно. Например, у Вас в каталоге есть файлы "rm", "-Rf", а вы матчите список файлов шеллом (через "*"). Или просто список имён файлов вылезает за пределы определенного лимита...
По статье — для начала неплохо. Кейс действительно интересный. Но очень рекомендую исправить все опечатки/описки, т.к. выглядит неаккуратненько.
IvanNochnoy
Как вариант:
mayorovp
Этот вариант тоже повиснет если выходной файл уже существует.