Что надо было сделать


Однажды мне понадобилось объединить пачку текстовых файлов, лежащих в одной директории, в один файл. Делать руками этого не хотелось и мне на помощь, как всегда, пришёл Гугл! Я много слышал о мощности такого средства, как PowerShell, и решил использовать его для этой "мегазадачи". Хотя бы потому, что с убогостью средства cmd.exe я знаком не по наслышке. Ну а делать это руками — не наш путь.


Что пошло не так


Гугл подсказал мне, что сделать это можно простой командой


Get-ChildItem -Filter *.log | Get-Content | Out-File result.txt

"Действительно круто! Просто Unix-way какой-то!" — подумал я. Скопировал команду, слегка модифицировал её и нажал Enter. Курсор перешёл на новую строку… и больше ничего. В файловом менеджере я открыл итоговый файл — в нём действительно было что-то похожее на нужный результат. В нём было много строк из исходных файлов. Вернувшись в консоль я увидел, что процесс всё ещё… в процессе. Помог Ctrl+C.


Присмотревшись к размеру файла я увидел, что он как-то подозрительно велик. ЕГо размер превышел 100 Мегабайт. Хотя водные данные не были такими большими.


Почему это случилось?


Всё дело в моей "лёгкой модификации". Мне просто не нужен был фильтр по расширению. Да и параметр этот не является обязательным. И получилось, что команда создала результирующий файл, увидела, что он есть в директории, прочитала его и снова записала своё содержимое в конец и делала это, пока я не нажал Ctrl+C Никак по другому непрерывный рост вызодного файла я объянить для себя не смог


Я повторил это в "стерильных" условиях. Для простоты и чистоты эксперимента делаю в отдельной директории, так как боюсь убить рабочую машину


  1. Создаю текстовый файл


    echo "Hello world" > hello.txt

  2. Выполняю команду


    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)


  1. IvanNochnoy
    19.12.2018 18:21

    Как вариант:


    @(dir) | cat > result.txt


    1. mayorovp
      20.12.2018 14:51

      Этот вариант тоже повиснет если выходной файл уже существует.


  1. dipsy
    20.12.2018 19:31
    +1

    Убогость cmd.exe конечно поражает, приходится аж что-то вот такое писать:
    for %f in (*.txt) do type "%f" >> output.txt
    Причем оно даже результат к самому себе не дописывает в цикле, а вдруг нам именно это и нужно было


    1. Nitrofen
      20.12.2018 01:06

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


      1. mayorovp
        20.12.2018 09:13

        Просто обязательный префикс для параметров и переменных цикла. Почти то же самое, что и знак $ в баше, powershell или php.


  1. 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 Здесь до меня по сути то же самое написали.


    1. mayorovp
      20.12.2018 09:20

      Ваше (ls | cat) > out.txt — это ровно то же самое, что и авторское get-childitem | get-content | out-file result.txt, только выходной файл называется по-другому.


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


      1. mayorovp
        20.12.2018 14:49

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

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


  1. Khabarovchanin
    20.12.2018 21:33

    убогостью средства cmd.exe

    Хм…
    for %R in (*.txt) do  type %R >> result.txt


    1. dipsy
      20.12.2018 05:24

      А если эту строку вписать в файл merge.cmd и запустить, сработает? У меня почему-то только в консоли cmd такая строка работает, а если батником запускать, находящимся в папке с требуемыми файлами, никакого эффекта.


      1. remzalp
        20.12.2018 07:54

        в батнике надо % удваивать:


        for %%R in (*.txt) do @type %%R>> rezult.tx

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


      1. Deosis
        20.12.2018 07:59

        В батнике надо писать %% перед переменной.
        Серьезная подстава от языка.


  1. mistergrim
    20.12.2018 01:50

    copy *.txt result.txt


    1. dipsy
      20.12.2018 05:31

      Проверил, 3 текстовых файла в папке было, объединились, но результат получился на 1 байт больше суммы исходных, в конце файла символ с кодом 0x1A зачем-то дописался…


      1. mistergrim
        20.12.2018 08:19

        0x1A — это EOF. Legacy, ещё из DOS тянется, что поделать.
        Можно склеивать так — все символы 0x1a будут вырезаны, новые добавлены не будут:
        copy *.txt result.txt /b


  1. 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


  1. neitri
    20.12.2018 14:52

    Get-ChildItem -filter *.log | Where-Object Name -notlike 'result.log'| Get-Content |Out-File result.log


  1. YMax
    21.12.2018 07:37

    А зачем писать итоговый файл в одну папку с исходными и создавать проблему на пустом месте?


    1. solo12zw74 Автор
      21.12.2018 09:38

      Статья больше о том, что это поведение ненормальное и непредсказуемое. Путей решения есть много, если знаешь, что проблема есть.
      Вспомнился анекдот:

      Приходит мужик к врачу и говорит:
      — Доктор, я когда рукой двигаю, у меня вот тут болит
      — А когда не двигаете — не болит?
      — Нет.
      — Ну так просто не двигайте рукой!


      1. gecube
        21.12.2018 21:12

        Насчёт — ненормальное и непредсказуемое — Вы погорячились. Просто, видимо, опыта мало.
        Бомбануть может где угодно. Например, у Вас в каталоге есть файлы "rm", "-Rf", а вы матчите список файлов шеллом (через "*"). Или просто список имён файлов вылезает за пределы определенного лимита...


        По статье — для начала неплохо. Кейс действительно интересный. Но очень рекомендую исправить все опечатки/описки, т.к. выглядит неаккуратненько.