Robert MacLean
Robert MacLean

Reputation: 39261

Combining files using COPY command and getting odd characters

I have a bunch of small PowerShell functions, each in their own file and I am trying to combine them into a larger file.

Using the low-tech approach of the Windows copy command by doing: copy /Y /A functions\*.ps1 super.ps1 works fine however where two files are joined it is inserting: 

I'm guessing it's the line break characters showing up (difference of encoding), but how do I prevent these characters from showing up?

Upvotes: 5

Views: 4217

Answers (5)

Celes
Celes

Reputation: 37

I strongly advise you to NOT use DOS Batch to read/write binary files, it is not suited to do that, batch is very poor.

Here is a script that use a hack of "fc" and "certutil" to do that. But in only works on little files.

Be careful if you use this script, you will have to edit some characters with an hex editor.

cls
@echo off
cd /d "%~dp0"
chcp 65001 >nul
set title_script=Concaténer des fichiers en ignorant le BOM
title %title_script%



rem Création des fichiers de test du script.

set bom_utf8=""
rem Cette ligne est spéciale. Entre les guillemets se trouvent
rem les 3 caractères hexadécimaux EF BB BF du BOM UTF-8
rem que l'on a écrit avec un éditeur hexadécimal.

if exist "_File 1.txt" del /f /q "_File 1.txt"
if exist "_File 2.txt" del /f /q "_File 2.txt"
if exist "_File 3.txt" del /f /q "_File 3.txt"
if exist "_FUSION.txt" del /f /q "_FUSION.txt"
echo|set /p=%bom_utf8%> "_File 1.txt"
echo|set /p=%bom_utf8%> "_File 2.txt"
echo|set /p=%bom_utf8%> "_File 3.txt"
echo "Fichier 1" >> "_File 1.txt"
echo "Fichier 2" >> "_File 2.txt"
echo "Fichier 3" >> "_File 3.txt"



rem ___________________________________________________________________________

rem Liste des fichiers à concaténer.
set FILE_1=%~dp0_File 1.txt
set FILE_2=%~dp0_File 2.txt
set FILE_3=%~dp0_File 3.txt

rem Nom du fichier concaténé.
set OUTPUT_FILE=%~dp0_FUSION.txt

rem ___________________________________________________________________________

rem Liste des Byte Order Mark (BOM).
set "BOM_UTF8=EFBBBF"
set "BOM_UTF16_LE=FFFE"
set "BOM_UTF16_BE=FEFF"
set "BOM_UTF32_LE=FFFE0000"
set "BOM_UTF32_BE=0000FEFF"

rem Taille maximum d'un BOM, ici 4 octets avec l'UTF-32.
set /a BOM_MAX_SIZE=4

rem Fichiers temporaires pour les commandes "fc" et "certutil".
set "TEMP_FILE_1=%~dpn0_tmp_1.txt"
set "TEMP_FILE_2=%~dpn0_tmp_2.txt"
set "TEMP_FILE_OUTPUT_1=%~dpn0_tmp_3.txt"
set "TEMP_FILE_OUTPUT_2=%~dpn0_tmp_4.txt"

set error_message_1=Erreur, le fichier : "%FILE_1%" n'existe pas. ^& echo.Ce script va s'arrêter.
rem error_message_1=Error, the file: "%FILE_1%" doesn't exist. ^& echo.This script will stop.
set /a switch=0
:define_variable
set error_message_2=Erreur : Le Byte Order Mark de "%CURRENT_FILE%" est "%FILE_BOM%" au lieu de "%FIRST_FILE_BOM%". ^& echo.Ce script va s'arrêter.
rem set error_message_2=Error: The Byte Order Mark in "%CURRENT_FILE%" is "%FILE_BOM%" instead of "%FIRST_FILE_BOM%". ^& echo.This script will stop.
if %switch% EQU 1 (goto variable_defined)
set "end_message=Operation accomplie."
rem set "end_message=Operation completed."



echo en-US - If the files to concatenate have a Byte Order Mark, this script is limited to small files. From 10000 bytes, this script will create more than 1254 temporary files, the operation may take a long time. Do you want to continue ? Yes/No (Y/N)
echo.
echo fr-FR - Si les fichiers à concatener ont un Byte Order Mark, ce script se limite aux petits fichiers. À partir de 10000 octets, ce script va créer plus de 1254 fichiers temporaires, l'opération risque de durer longtemps. Voulez-vous continuer ? Oui/Non (Y/N)
echo.
echo.
set RESPONSE=
set /P RESPONSE=Type your answer: %=%
if "%RESPONSE%"=="y" (goto yes)
if "%RESPONSE%"=="n" (goto no)
if "%RESPONSE%"=="Y" (goto yes)
if "%RESPONSE%"=="N" (goto no)
:no
exit /b 0
:yes
echo.



rem Si le premier fichier n'existe pas, on stoppe le programme en renvoyant le code d'erreur -1.
if not exist "%FILE_1%" (
    echo %error_message_1%
    echo.
    pause
    exit /b -1
)



rem On détecte le BOM du premier fichier.
set "FILE_BOM=NONE"
call :my_function_read_bom FILE_1 FILE_BOM
set "FIRST_FILE_BOM=%FILE_BOM%"
echo %FILE_1% : %FIRST_FILE_BOM%

rem On crée le fichier de sortie.
copy /v /y /b "%FILE_1%" "%OUTPUT_FILE%" /b
echo.

rem S'il n'y a pas de BOM sur le premier fichier, c'est du binaire, de l'ASCII ou de l'ANSI, donc on copie tout.
rem On prend chaque fichier suivant avec une boucle while en utilisant un nom de variable dynamique (avec la commande "goto").
set /a count_loop=2
setlocal enabledelayedexpansion
set "CURRENT_FILE=!FILE_%count_loop%!"
rem Cette concaténation de variables ne fonctionne pas dans un bloc if.
:while_loop_1
    if "%FIRST_FILE_BOM%"=="NONE" (
        copy /v /y /b "%OUTPUT_FILE%" + /b "!CURRENT_FILE!" "%OUTPUT_FILE%" /b
    ) else (goto end_loop_1)
    set /a count_loop=count_loop+1
    set "CURRENT_FILE=!FILE_%count_loop%!"
    if exist "!CURRENT_FILE!" (goto while_loop_1)
    echo.
    echo %end_message%
    echo.
    pause
    exit /b 0
:end_loop_1
endlocal

rem S'il y a un BOM, il faut copier le contenu du fichier en le supprimant.
set /a count_loop=2
setlocal enabledelayedexpansion
set "CURRENT_FILE=!FILE_%count_loop%!"
:while_loop_2
    rem On vérifie le BOM.
    set "FILE_BOM=NONE"
    call :my_function_read_bom CURRENT_FILE FILE_BOM
    echo !CURRENT_FILE! : !FILE_BOM!

    rem Si le BOM est différent du fichier de départ, on stoppe avec le code d'erreur -2.
    if not "!FILE_BOM!"=="!FIRST_FILE_BOM!" (
        echo.
        set /a switch=1
        goto define_variable
        :variable_defined
        echo %error_message_2%
        echo.
        pause
        exit /b -2
    )

    rem On crée une copie temporaire du fichier à ajouter mais sans son BOM.
    if "!FILE_BOM!"=="UTF8" (set /a SIZE_OF_BOM=3)
    if "!FILE_BOM!"=="UTF16LE" (set /a SIZE_OF_BOM=2)
    if "!FILE_BOM!"=="UTF16BE" (set /a SIZE_OF_BOM=2)
    if "!FILE_BOM!"=="UTF32LE" (set /a SIZE_OF_BOM=4)
    if "!FILE_BOM!"=="UTF32BE" (set /a SIZE_OF_BOM=4)
    if "!FILE_BOM!"=="NONE" (exit /b -3)
    call :my_function_split_file CURRENT_FILE SIZE_OF_BOM

    rem On ajoute cette copie temporaire.
    copy /V /Y /B "%OUTPUT_FILE%" + /B "!CURRENT_FILE!_part.2" "%OUTPUT_FILE%" /B
    echo.

    rem Puis on supprime cette copie temporaire.
    del /F /Q "!CURRENT_FILE!_part.2" >nul 2>&1

    rem Tous les programmes (donc toutes les commandes) ont 3 flux :
    rem Standard Input  = Le flux des entrées (clavier pour la console)
    rem Standard Output = Le flux des sorties (interface utilisateur et écran)
    rem Standard Error  = Le flux des erreurs (messages d'erreur affichés)
    rem L'indicatif 1>nul ou par défaut >nul annule le flux des sorties, la commande n'affiche rien sauf en cas d'erreur.
    rem L'indicatif 2>nul annule le flux des erreurs, la commande affiche tout sauf les erreurs.
    rem L'indicatif 2>&1  permet de réorienter le flux des erreurs sur le flux des sorties.
    rem Donc >nul 2>&1 permet de ne jamais rien afficher, même en cas d'erreur.

    set /a count_loop=count_loop+1
    set CURRENT_FILE=!FILE_%count_loop%!
    if exist "%CURRENT_FILE%" (goto while_loop_2)
endlocal



echo.
echo %end_message%
echo.
pause

rem Destruction des fichiers de test du script.
if exist "%FILE_1%" del /f /q "%FILE_1%"
if exist "%FILE_2%" del /f /q "%FILE_2%"
if exist "%FILE_3%" del /f /q "%FILE_3%"
if exist "%OUTPUT_FILE%" del /f /q "%OUTPUT_FILE%"

exit /b 0
rem Il ne faut pas oublier le "exit" sinon les fonctions qui suivent seront à nouveau lues comme du code simple et cela causera des problèmes.

rem ___________________________________________________________________________

:my_function_read_bom
    setlocal enabledelayedexpansion
    set "FILE_INPUT=!%~1!"

    rem On supprime le fichier temporaire s'il existe déjà.
    del /f /q "%TEMP_FILE_1%" >nul 2>&1
    del /f /q "%TEMP_FILE_OUTPUT_1%" >nul 2>&1

    rem On crée le fichier temporaire et on le remplit de zéros selon la taille à comparer (soit 4 octets à 00).
    rem Goto eof sert à arrêter si le fichier temporaire n'a pas pu être créé.
    fsutil file createnew "%TEMP_FILE_1%" !BOM_MAX_SIZE! >nul || goto :eof

    set /a COUNT_LINE=1
    >"%TEMP_FILE_OUTPUT_1%" (
        rem Pour lire le fichier en binaire, on utilise un hack de la commande "fc".
        rem La commande "fc" permet d'afficher une comparaison binaire octet par octet entre deux fichiers.
        rem Elle n'affiche que les octets qui ne correspondent pas, donc elle n'affichera pas les zéros.
        rem On utilise la boucle "for /f" pour extraire les octets du résultats de la commande "fc".
        rem Avec "skip", on saute la première ligne.
        rem Avec "delims", on sépare chaque ligne selon les caractères deux-points et espace.
        rem Avec "tokens", on place le contenu séparé dans 2 variables (i et j).
        rem Donc la variable i contiendra l'offset et la variable j contiendra l'octet.
        for /f "skip=1 tokens=1,2 delims=: " %%i in ('fc /b "%FILE_INPUT%" "%TEMP_FILE_1%"') do (
            rem À la fin, la commande FC affiche un comparatif de taille qui commence par des caractères
            rem dont les octets sont "46 43 EFBFBD", ils sont inscrits ici avec un éditeur hexadécimal.
            if not "%%i"=="FC�" (
                set /a OFFSET=0x%%i

                rem Si l'octet est 00, la commande "fc" ne les affiche pas.
                for /l %%k in (!COUNT_LINE!,1,!OFFSET!) do (<nul set /p "=00")

                rem On écrit l'octet dans le fichier.
                <nul set /p "=%%j"

                set /a COUNT_LINE=OFFSET+2
            )
        )
        rem Si les derniers octets sont 00, la commande "fc" ne les affiche pas.
        for /l %%i in (!COUNT_LINE!,1,!BOM_MAX_SIZE!) do (<nul set /p "=00")
    )
    del /f /q "%TEMP_FILE_1%" >nul 2>&1

    rem Le fichier de sortie contient les premiers octets du fichier à lire.
    rem On lit les premiers caractères de ce fichier de sortie.
    rem On utilise la boucle "for /f" pour lire le fichier ligne par ligne.
    rem Avec "usebackq", on peut lire les noms de fichiers ayant des espaces.
    for /f "usebackq tokens=* delims=" %%i in ("%TEMP_FILE_OUTPUT_1%") do (
        set "LINE_READ=%%i"

        rem On en déduit le BOM du fichier.
        if "!FILE_BOM!"=="NONE" (
            set BOM_READ=!LINE_READ:~0,8!
            if !BOM_READ!==%BOM_UTF32_LE% (set "FILE_BOM=UTF32LE")
        )
        if "!FILE_BOM!"=="NONE" (
            set BOM_READ=!LINE_READ:~0,8!
            if !BOM_READ!==%BOM_UTF32_BE% (set "FILE_BOM=UTF32BE")
        )
        if "!FILE_BOM!"=="NONE" (
            set BOM_READ=!LINE_READ:~0,6!
            if !BOM_READ!==%BOM_UTF8% (set "FILE_BOM=UTF8")
        )
        if "!FILE_BOM!"=="NONE" (
            set BOM_READ=!LINE_READ:~0,4!
            if !BOM_READ!==%BOM_UTF16_LE% (set "FILE_BOM=UTF16LE")
        )
        if "!FILE_BOM!"=="NONE" (
            set BOM_READ=!LINE_READ:~0,4!
            if !BOM_READ!==%BOM_UTF16_BE% (set "FILE_BOM=UTF16BE")
        )
    )
    del /f /q "%TEMP_FILE_OUTPUT_1%" >nul 2>&1
    endlocal & set %2=%FILE_BOM%
goto :eof

:my_function_split_file
    rem On importe le nom du fichier et on retire les guillemets.
    set "INPUT_FILE=!%~1!"
    set /a BUFFER=7800

    rem On supprime les fichiers temporaires.
    rem Le préfixe "/a:h" est indispensable pour pouvoir aussi supprimer les fichiers cachés.
    del /f /q "%TEMP_FILE_2%" >nul 2>&1
    del /f /q /a:h "%TEMP_FILE_OUTPUT_2%_part.*" >nul 2>&1

    rem Pour écrire dans un fichier en binaire, on utilise un hack de la commande "certutil".
    rem Cette commande crée un fichier texte plus gros que le fichier d'origine.
    rem La commande crée un fichier texte contenant une vue du fichier à la façon d'un éditeur hexadécimal.

    rem Elle découpe le fichier en lignes de 16 octets. Chaque ligne se compose de...
    rem Trois caractères qui indiquent un numéro de ligne : 000, 001, 002... 009, 00a, 00b... 00f, 010, 011, 012... fff.
    rem La commande est donc limitée à 4096 lignes (de 0 à fff). Donc la taille du fichier est limitée à 65536 octets.
    rem Puis il y a un 0 (30) et une tabulation (09).
    rem Puis le code hexadécimal est écrit au format ASCII en deux groupes de 8 octets séparés par un espace.
    rem Puis il y a 3 espaces (ou plus, pour que la partie suivante arrive à la colonne 57 du fichier texte).
    rem Puis le code binaire du fichier est recopié en remplaçant chaque caractère non-textuel par un point (2E).
    rem Exemple d'un fichier en UTF-8 contenant la ligne "abcdefghijklm".
    rem Cela donne "0000    EF BB BF 61 62 63 64 65  66 67 68 69 6a 6b 6c 6d   ...abcdefghijklm".
    rem Enfin la ligne se termine par un retour à la ligne (0D0A).

    rem Le paramètre "f" sert à écraser le fichier existant (force overwrite).
    certutil -encodehex -f "%INPUT_FILE%" "%TEMP_FILE_2%" >nul

    rem SIZE est le nombre d'octets du BOM qu'il faudra retirer.
    set /a "SIZE=!%~2!"

    if not defined SIZE (echo SIZE undefined. & goto :eof)
    if !SIZE! LEQ 0 (echo SIZE not valid. & goto :eof)

    rem LENGTH la longueur du BOM en nombre de caractères.
    rem Par exemple en UTF-8, EFBBBF prend 3 octets mais 6 caractères.
    rem Il faut multiplier par 2 car un octet s'écrit avec 2 caractères.
    set /a LENGTH=%SIZE%*2

    set "HEXADECIMAL_STRING="
    set /a HEXADECIMAL_STRING_LENGTH=0
    set /a PART=1

    rem On lit le fichier texte créé, et on le découpe en mettant chaque ligne en un fichier différent.
    rem Le délimitateur est le caractère "Tabulation" (09).
    for /f "usebackq tokens=2 delims=   " %%a in ("%TEMP_FILE_2%") do (
        set "CURRENT_LINE=%%a"

        rem Chaque ligne fait 72 caractères de long. 4 pour le numéro de ligne,
        rem 1 pour la tabulation, 48 pour la partie hexadécimale et 19 pour le texte.
        rem On extrait les 48 caractères pour n'obtenir que la partie hexadécimale.
        rem Avec notre exemple, on obtient donc : "ef bb bf 61 62 63 64 65  66 67 68 69 6a 6b 6c 6d".
        set HEXADECIMAL_STRING=!HEXADECIMAL_STRING!!CURRENT_LINE:~0,48!

        rem On supprime les espaces, ce qui fait 32 caractères.
        rem Avec notre exemple, on obtient donc : "efbbbf6162636465666768696a6b6c6d".
        set HEXADECIMAL_STRING=!HEXADECIMAL_STRING: =!

        if !HEXADECIMAL_STRING_LENGTH! GEQ 32 (
            set /a HEXADECIMAL_STRING_LENGTH=HEXADECIMAL_STRING_LENGTH+32
        )
        if !HEXADECIMAL_STRING_LENGTH! LSS 32 (
            call :function_strlen HEXADECIMAL_STRING HEXADECIMAL_STRING_LENGTH
        )
        rem Nombre de caractères que le fichier contient / Nombre maximum que l'on peut en extraire.
        echo %INPUT_FILE% : !HEXADECIMAL_STRING_LENGTH! / %BUFFER%

        rem Il faut extraire le nombre de caractères LENGTH.
        rem La ligne contient HEXADECIMAL_STRING_LENGTH caractères.
        rem Si la taille de la ligne est >= la taille du BOM à extraire,
        rem alors le BOM à extraire est entièrement contenu dans la ligne,
        rem donc on sauvegarde la partie à extraire dans un fichier.
        if !HEXADECIMAL_STRING_LENGTH! GEQ !LENGTH! (
            set /a REST=HEXADECIMAL_STRING_LENGTH-LENGTH
            for %%i in (!REST!) do (
                echo(!HEXADECIMAL_STRING:~0,-%%i!>>"%TEMP_FILE_OUTPUT_2%_part.!PART!"
                rem Avec notre exemple, le fichier contient donc les caractères
                rem "efbbbf" suivi d'un retour à la ligne (0D0A).

                rem On cache le fichier.
                attrib +h "%TEMP_FILE_OUTPUT_2%_part.!PART!" >nul

                set HEXADECIMAL_STRING=!HEXADECIMAL_STRING:~-%%i!
                rem La variable contient désormais le reste de la ligne,
                rem dans notre exemple : "6162636465666768696a6b6c6d".

                set /a HEXADECIMAL_STRING_LENGTH=REST
            )
            rem On utilise "certutil" pour créer un fichier binaire à partir du fichier texte.
            certutil -decodehex -f "%TEMP_FILE_OUTPUT_2%_part.!PART!" "%INPUT_FILE%_part.!PART!" >nul

            set /a PART=PART+1
            set /a LENGTH=%SIZE%*2
        )

        rem Si la taille de la ligne est >= la taille maximum de sécurité,
        rem on sauvegarde la ligne dans un fichier et on réinitialise.
        if !HEXADECIMAL_STRING_LENGTH! GEQ !BUFFER! (
            echo(!HEXADECIMAL_STRING!>>"%TEMP_FILE_OUTPUT_2%_part.!PART!"
            attrib +h "%TEMP_FILE_OUTPUT_2%_part.!PART!" >nul
            set "HEXADECIMAL_STRING="
            set /a HEXADECIMAL_STRING_LENGTH=0

            set /a LENGTH=!LENGTH!-!BUFFER!
            if !LENGTH! LSS 0 (set /a LENGTH=0)
        )
    )
    rem Au final, il reste la variable contenant le reste de la ligne, on l'extrait dans un autre fichier.
    echo !HEXADECIMAL_STRING!>>"%TEMP_FILE_OUTPUT_2%_part.!PART!"
    attrib +h "%TEMP_FILE_OUTPUT_2%_part.!PART!" >nul
    certutil -decodehex -f "%TEMP_FILE_OUTPUT_2%_part.!PART!" "%INPUT_FILE%_part.!PART!" >nul

    del /f /q "%TEMP_FILE_2%" >nul 2>&1
    del /f /q /a:h "%TEMP_FILE_OUTPUT_2%_part.*" >nul 2>&1

    rem On fusionne toutes les autres parties dans la deuxième.
    if !PART! GEQ 3 (
        for /l %%i in (3,1,!PART!) do (
            copy /v /y /b "%INPUT_FILE%_part.2" + /b "%INPUT_FILE%_part.%%i" "%INPUT_FILE%_part.2" /b >nul
            del /f /q "%INPUT_FILE%_part.%%i" >nul 2>&1
        )
    )

    rem On supprime la première partie.
    del /f /q "%INPUT_FILE%_part.1" >nul 2>&1
goto :eof

:function_strlen
    setlocal enabledelayedexpansion
    set "str=!%~1!#"
    set "length=0"
    for %%i in (8184 4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
        if "!str:~%%i,1!" NEQ "" ( 
            set /a "length+=%%i"
            set "str=!str:~%%i!"
        )
    )
    endlocal & set "%~2=%length%"
goto :eof

Upvotes: 0

The Disintegrator
The Disintegrator

Reputation: 4187

Those characters are the byte order mark (BOM) as Steve Gilham said. The easiest thing you can do to remove the BOM is to open all the files in a text editor that supports utf-8, make a small edit and save again. Gearny is an ideal candidate for this. It supports utf-8, doesn't save the BOM and it's free. It's only for windows, but I'm assuming you have access to a windows box. I don't know any dos program capable of strip the BOM (in the DOS era utf-8 was only a project)

Once you stripped the BOM you can use the copy method but in binary mode (/b), otherwise the most possible outcome will be garbled text whenever a multibyte character appears.

Upvotes: 0

kingchris
kingchris

Reputation: 1757

Here is an interesting direction to take

http://msdn.microsoft.com/en-us/library/aa368046(VS.85).aspx (can't get this sucker to link)

This has two solutions to copy your PowerShell scripts which appear to be Unicode to ANSI. One solution written in VB the other in PowerShell

Once in Ansi follow everyones recommendation

Upvotes: 1

kingchris
kingchris

Reputation: 1757

How you tried /B for binary to stop OEM characterset translation

Who knows

Or open your scripts in notepad and save them as ANSI then try and join them with your normal copy command no /B required

Upvotes: 4

Steve Gilham
Steve Gilham

Reputation: 11277

Those characters are unicode byte order marks which will be invisibly preceding the .ps1 content.

I'm not sure how you'd strip them off in plain DOS -- at this point I'd turn to a scripting language to do the work. You could write a PowerShell script to join them using the .net System.IO features.

Upvotes: 3

Related Questions