1 (изменено: Rumata, 2021-10-18 12:18:51)

Тема: CMD/BAT: Определение кодировки файла (определение BOM)


**********************
Скрипт имеет ограничение: он умеет определять кодировку только по наличию BOM. Для UTF-8 его наличие необязательно. Поэтому UTF-8 файлы без BOM не определяются. В процессе изобретения этого скрипта был найден еще один вариант. Он приведен в этой же теме ниже.
**********************

Сходная тема в соседнем разделе WSH: Определение кодировки файла

Используются только возможности интерпретатора и стандартная команда FC для сравнения двух файлов. Хитрость заключается в двоичном сравнении специально созданного временного файла с искомым. Идея не моя, я ее вычитал где-то на форуме https://www.dostips.com/.

Пример использования:


>powershell -c "write \"In math the Greek letter $([char]0x03C0) stands for 3.1415926\" | out-file -encoding utf8 example-file"

>type example-file
я╗┐In math the Greek letter ╧А stands for 3.1415926

>file-detect-bom.bat example-file
example-file: UTF-8

>file-detect-bom.bat -b example-file
UTF-8
+ исходный код

Актуальну версию можно найти здесь: https://github.com/ildar-shaimordanov/c … ct-bom.bat


::Usage: file-detect-bom [OPTIONS] FILE...
::
::Detect the Byte Order Mark (BOM) in FILEs.
::
::  -b, --brief  Don't prepend filenames to output

@echo off

setlocal enabledelayedexpansion

set "bom_brief="
if /i "%~1" == "-b"      set "bom_brief=1"
if /i "%~1" == "--brief" set "bom_brief=1"
if defined bom_brief shift /1

if "%~1" == "" goto :print_usage

:: ========================================================================

:: The following settings are based on information from the table
:: https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
set "bom_val_EFBBBF=UTF-8"
set "bom_val_FEFF=UTF-16BE"
set "bom_val_FFFE=UTF-16LE"
set "bom_val_0000FEFF=UTF-32BE"
set "bom_val_FFFE0000=UTF-32LE"
set "bom_val_2B2F76=UTF-7"
set "bom_val_F7644C=UTF-1"
set "bom_val_DD736673=UTF-EBCDIC"
set "bom_val_0EFEFF=SCSU"
set "bom_val_FBEE28=BOCU-1"
set "bom_val_84319533=GB-18030"

:: ========================================================================

set "bom_cmpfile=%TEMP%\bom_cmpfile"
set /p "=@@@@" <nul >"%bom_cmpfile%"

:bom_begin_loop
if "%~1" == "" (
	del /f /q "%bom_cmpfile%" 2>nul
	goto :EOF
)

for %%f in ( "%~1" ) do (
	call :detect_type "%%~f"

	if defined bom_brief (
		if defined bom_found echo:!bom_found!
	) else (
		echo:%%~f: !bom_found!
	)
)

shift /1
goto :bom_begin_loop

:: ========================================================================

:print_usage
for /f "usebackq tokens=* delims=:" %%s in ( "%~f0" ) do (
	if /i "%%s" == "@echo off" goto :EOF
	echo:%%s
)
goto :EOF

:: ========================================================================

:detect_type
set "bom_found="

set "bom_srcfile=%~1"

if not exist "%~1" (
	echo:File not found: "%~1">&2
	exit /b 1
)
if exist "%~1\" (
	set "bom_found=directory"
	goto :EOF
)
if %~z1 equ 0 (
	set "bom_found=empty"
	goto :EOF
)

set "bom_bytes="
for /f "tokens=1,2,3,4 delims=: " %%a in ( '
	fc /b "%bom_cmpfile%" "%~1" ^| findstr /n /r "^00*[0-3]"
' ) do (
	set /a bom_diff=%%a-%%b
	if !bom_diff! equ 2 set "bom_bytes=!bom_bytes!%%d"
)

for /l %%n in ( 8, -2, 4 ) do for %%s in ( bom_val_!bom_bytes:~0^,%%n! ) do (
	set "bom_found=!%%s!"
	if defined bom_found goto :EOF
)
goto :EOF

:: ========================================================================

:: EOF
+ история изменений

2021-10-14
-- несколько косметических изменений
-- "молчаливое" удаление временного файла
-- определение кодировки оформлено как подпрограмма
-- корректно сообщает о несуществующих файлах, пустых файлах и директориях
-- переименовал скрипт: "bom_file.bat" -> "file-detect-bom.bat"

2021-10-13
После первой публикации нашел небольшой баг: если кодировка файла не определена, то следущий файл отображается в этой же строке. Например:


Z:>bom_file ascii_file utf8_file
ascii_file: utf8_file: UTF-8

Сейчас это исправлено.

Потом мне захотелось сделать скрипт еще лучше и я изменил несколько мест:
-- немного усложнил, но сделал более строгим сбор первых, не более 4 последовательных байт
-- оптимизировал фрагмент определения кодировки
-- упростил вывод результата

2021-10-11
Первая публикация

Историю изменений можно посмотреть по этим ссылкам:
-- https://github.com/ildar-shaimordanov/c … m_file.bat.
-- https://github.com/ildar-shaimordanov/c … ct-bom.bat

Я намеренно каждое исправление фиксировал отдельным коммитом.

( 2 * b ) || ! ( 2 * b )

2 (изменено: Rumata, 2021-10-15 00:56:39)

Re: CMD/BAT: Определение кодировки файла (определение BOM)

Внесены изменения

Rumata пишет:

2021-10-14
-- несколько косметических изменений
-- "молчаливое" удаление временного файла
-- определение кодировки оформлено как подпрограмма
-- корректно сообщает о несуществующих файлах, пустых файлах и директориях
-- переименовал скрипт: "bom_file.bat" -> "file-detect-bom.bat"

( 2 * b ) || ! ( 2 * b )

3 (изменено: Rumata, 2021-10-18 12:44:51)

Re: CMD/BAT: Определение кодировки файла (определение BOM)


**********************
Данный скрипт умеет определять кодировку файла по наличию BOM. Если BOM отсутствует, файл анализируется на наличие правильных последовательностей байт, соответствующих кодам в UTF-8. При наличии таких последовательностей файл определяется как UTF-8.
**********************

В отличие от предыдущего срипта здесь используется команда certutil -encodehex, которая создает временный файл - дамп оригинального файла, где каждый байт конвертирован в его 16-ричное представление. В дальнейшем скрипт работает с этими данными.

Пример:


>type example-utf8-with-bom.txt
я╗┐In math the Greek letter ╧А stands for 3.1415926

>type example-utf8-without-bom.txt
In math the Greek letter ╧А stands for 3.1415926

>file-detect-enc.bat example-utf8-*.txt
example-utf8-with-bom.txt: UTF-8
example-utf8-without-bom.txt: UTF-8
+ исходный код

Актуальная версия здесь: https://github.com/ildar-shaimordanov/c … ct-enc.bat


::Usage: file-detect-enc [OPTIONS] FILE...
::
::Detect type (encoding) of FILEs.
::
::  -b, --brief  Don't prepend filenames to output

@echo off

setlocal enabledelayedexpansion

set "enc_brief="
if /i "%~1" == "-b"      set "enc_brief=1"
if /i "%~1" == "--brief" set "enc_brief=1"
if defined enc_brief shift /1

if "%~1" == "" goto :print_usage

:: ========================================================================

:: The following settings are based on information from the table
:: https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
set "enc_val_EFBBBF=UTF-8"
set "enc_val_FEFF=UTF-16BE"
set "enc_val_FFFE=UTF-16LE"
set "enc_val_0000FEFF=UTF-32BE"
set "enc_val_FFFE0000=UTF-32LE"
set "enc_val_2B2F76=UTF-7"
set "enc_val_F7644C=UTF-1"
set "enc_val_DD736673=UTF-EBCDIC"
set "enc_val_0EFEFF=SCSU"
set "enc_val_FBEE28=BOCU-1"
set "enc_val_84319533=GB-18030"

:: ========================================================================

set "enc_hexfile=%TEMP%\enc_hexfile"

:enc_begin_loop
if "%~1" == "" (
	del /f /q "%enc_hexfile%" 2>nul
	goto :EOF
)

for %%f in ( "%~1" ) do (
	call :detect_type "%%~f"

	if defined enc_brief (
		if defined enc_found echo:!enc_found!
	) else (
		echo:%%~f: !enc_found!
	)
)

shift /1
goto :enc_begin_loop

:: ========================================================================

:print_usage
for /f "usebackq tokens=* delims=:" %%s in ( "%~f0" ) do (
	if /i "%%s" == "@echo off" goto :EOF
	echo:%%s
)
goto :EOF

:: ========================================================================

:detect_type
set "enc_found="

set "enc_srcfile=%~1"

if not exist "%~1" (
	echo:File not found: "%~1">&2
	exit /b 1
)
if exist "%~1\" (
	set "enc_found=directory"
	goto :EOF
)
if %~z1 equ 0 (
	set "enc_found=empty"
	goto :EOF
)

:: https://stackoverflow.com/a/16238102/3627676
:: https://ss64.com/nt/certutil.html
:: https://www.dostips.com/forum/viewtopic.php?p=57918#p57918
:: https://docs.microsoft.com/en-gb/windows/win32/api/wincrypt/nf-wincrypt-cryptbinarytostringa
certutil -encodehex -f "%enc_srcfile%" "%enc_hexfile%" 4 >nul || (
	echo:Internal error: !errorlevel!>&2
	exit /b 1
)

set "enc_utf8_sequence="
set /a enc_utf8_require=0

set "enc_firstline=1"
for /f "usebackq delims=" %%s in ( "%enc_hexfile%" ) do (
	rem Most files (especially binaries) have in their beginning the
	rem magic number, or header, the group of bytes identifying the
	rem file type. Here we can analyze the header for magic number
	rem existence and quit immediately, if it's found. Otherwise,
	rem we continue analysis with the same line.
	if defined enc_firstline for /f "usebackq tokens=1-4" %%a in ( '%%s' ) do (
		set "enc_firstline="
		set "enc_bytes=%%a%%b%%c%%d"

		for /l %%n in ( 8, -2, 4 ) do for %%s in (
			enc_val_!enc_bytes:~0^,%%n!
		) do (
			set "enc_found=!%%s!"
			if defined enc_found goto :EOF
		)
	)

	rem https://en.wikipedia.org/wiki/UTF-8#Encoding
	rem 0000-007f		00-7f	-----	-----	-----
	rem 0080-07ff		c0-df	80-bf	-----	-----
	rem 0800-ffff		e0-ef	80-bf	80-bf	-----
	rem 10000-10ffff	f0-f7	80-bf	80-bf	80-bf
	for %%b in ( %%s ) do if 0x%%b lss 0x80 (
		rem 00-7f
		set "enc_utf8_sequence="
		set /a enc_utf8_require=0
	) else if 0x%%b gtr 0xf7 (
		rem f8-ff
		set "enc_utf8_sequence="
		set /a enc_utf8_require=0
	) else (
		rem 80-f7
		set "enc_utf8_sequence=!enc_utf8_sequence!%%b"

		if 0x%%b geq 0xf0 (
			rem f0-f7
			set /a enc_utf8_require=3
		) else if 0x%%b geq 0xe0 (
			rem e0-ef
			set /a enc_utf8_require=2
		) else if 0x%%b geq 0xc0 (
			rem c0-df
			set /a enc_utf8_require=1
		) else if !enc_utf8_require! gtr 0 (
			rem 80-bf
			set /a enc_utf8_require-=1
			if !enc_utf8_require! equ 0 (
				set "enc_found=UTF-8"
				goto :EOF
			)
		)
	)
)
goto :EOF

:: ========================================================================

:: EOF
( 2 * b ) || ! ( 2 * b )