Как объединить содержимое файлов в PowerShell. И при этом не пострадать

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


    Однажды мне понадобилось объединить пачку текстовых файлов, лежащих в одной директории, в один файл. Делать руками этого не хотелось и мне на помощь, как всегда, пришёл Гугл! Я много слышал о мощности такого средства, как 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)! И я, конечно, буду продолжать им пользоваться.

    Поделиться публикацией

    Комментарии 26

      +2

      Как вариант:


      @(dir) | cat > result.txt
        0
        Этот вариант тоже повиснет если выходной файл уже существует.
        +4
        Убогость cmd.exe конечно поражает, приходится аж что-то вот такое писать:
        for %f in (*.txt) do type "%f" >> output.txt
        Причем оно даже результат к самому себе не дописывает в цикле, а вдруг нам именно это и нужно было
          0
          всю жизнь пытался понять как люди понимают что там написано. вот что значит этот гребаный знак % вот просто к чему он там?
            +1
            Просто обязательный префикс для параметров и переменных цикла. Почти то же самое, что и знак $ в баше, powershell или php.
          0

          Не спец по 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

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


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

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

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

            Хм…
            for %R in (*.txt) do  type %R >> result.txt
              0
              А если эту строку вписать в файл merge.cmd и запустить, сработает? У меня почему-то только в консоли cmd такая строка работает, а если батником запускать, находящимся в папке с требуемыми файлами, никакого эффекта.
                +1

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


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

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

                  0

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

                +1
                copy *.txt result.txt
                  0
                  Проверил, 3 текстовых файла в папке было, объединились, но результат получился на 1 байт больше суммы исходных, в конце файла символ с кодом 0x1A зачем-то дописался…
                    +1
                    0x1A — это EOF. Legacy, ещё из DOS тянется, что поделать.
                    Можно склеивать так — все символы 0x1a будут вырезаны, новые добавлены не будут:
                    copy *.txt result.txt /b
                  0
                  х его з, но изначальная строчка будет работать нормально как задумано:
                  Get-ChildItem -Filter *.log | Get-Content | Out-File result.txt

                  Вот если бы result.txt был result.log, тогда да, еще возможна такая ситуация. Не знаю почему вам не был очевиден такой момент. Он же будет по конвееру передавать все по одному элементу, попадающему в шаблон, поэтому да, вечный loop. Можно еще так:
                  dir | cat > result.txt
                    0
                    Get-ChildItem -filter *.log | Where-Object Name -notlike 'result.log'| Get-Content |Out-File result.log
                      0

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

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

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


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

                        –1
                        Убогость это вот это:
                        Get-ChildItem -filter *.log | Where-Object Name -notlike 'result.log'| Get-Content |Out-File result.log

                        Против вот этого:
                        copy *.txt result.txt /b

                        (код взять из соседних комментов).
                          0
                          (ls *.txt) | cat > result.txt
                            0
                            В Линуксе вообще с командной строкой всё хорошо.
                              0
                              Похоже, про алиасы и сокращённый синтаксис Вы не в курсе. Это PowerShell. Теперь с ним в Линуксе с командной строкой ещё лучше.
                                –1
                                Обознатушки — перепрятушки.
                                В курсе — никсы благотворно влияют на продукцию МС, а то бы до сих пор нужно было бы выписывать «Войну и Мир» в командой строке. Жаль алиасы не везде есть, но их можно же определять вручную.
                          +1
                          Странно что никто не вспомнил про -Exclude:
                          ls *.log -Exclude 'result.log' | cat > 'result.log'

                          полная версия:
                          Get-ChildItem -Filter *.log -Exclude 'result.log'| Get-Content | Out-File result.log

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое