binary — 이진 문자열에서 필드 만들고 추출하기
binary — 이진 문자열에서 필드 만들고 추출하기
네트워크 프로토콜이나 파일 형식을 다루다 보면 바이트 단위의 이진 데이터를 다뤄야 할 때가 많아요. binary 명령은 일반 Tcl 값으로 이진 문자열을 만들고(format), 이진 문자열에서 데이터를 뽑아내고(scan), base64·hex·uuencode 같은 문자열 인코딩으로 변환(encode/decode)하는 작업을 한곳에서 처리합니다.
본문
binary decode format ?-option value ...? data
binary encode format ?-option value ...? data
binary format formatString ?arg arg ...?
binary scan string formatString ?varName varName ...?
이 명령은 이진 데이터를 다루는 기능을 제공합니다. binary format 서브명령은 일반 Tcl 값에서 이진 문자열을 만들어요. 예를 들어 16과 22라는 값이 주어졌을 때 32비트 아키텍처라면, 각각 4바이트 정수 두 개로 이루어진 8바이트 이진 문자열을 만들 수 있습니다. binary scan은 그 반대로, 이진 문자열에서 데이터를 추출해 일반 Tcl 문자열 값으로 돌려줍니다. binary encode와 binary decode는 이진 데이터를 base64 같은(예: MIME 메시지에서 쓰이는) 문자열 인코딩으로, 또는 그 반대로 변환합니다.
기타 이진 데이터 작업(부분 수열, 길이, 특정 인코딩의 문자열로 재해석)은 다른 Tcl 명령이 담당해요. 각각 예시로 string range, string length, encoding convertfrom이 그것입니다. Tcl의 이진 문자열은 그에 포함된 모든 문자가 \u0000-\u00FF 범위에 있는 문자열일 뿐입니다.
BINARY ENCODE와 DECODE
이진 데이터를 읽을 수 있는 문자열로 인코딩할 때, 시작 이진 데이터를 binary encode 명령에 인코딩 이름과 인코딩별 옵션과 함께 전달합니다. 인코딩된 데이터는 binary decode로 다시 이진 형태로 변환할 수 있어요. 다음 형식과 옵션이 지원됩니다.
base64
base64 인코딩은 메일 메시지와 XML 문서에서 흔히 쓰이며, 주로 대소문자 알파벳과 숫자를 사용합니다. 정보 손실 없이 임의로 재줄바꿈할 수 있다는 특징이 있어요. 인코딩 중 지원 옵션:
- -maxlen length — 출력을 length자를 넘지 않는 줄로 나누게 합니다. 기본은 줄을 나누지 않는 것.
- -wrapchar character — -maxlen으로 줄을 나눌 때 character를 줄 구분자로 쓰게 합니다. 기본은 개행 문자 "\n".
디코딩 중 지원 옵션:
- -strict — 인코딩 자체에 속하지 않는 문자를 만나면 오류를 던지게 합니다. 그렇지 않으면 무시합니다. RFC 2045는 base64 디코더가 비엄격(non-strict)하기를 권장합니다.
hex
hex 인코딩은 각 바이트를 그 바이트 값을 16진 정수로 나타내는 16진 숫자 한 쌍으로 변환합니다. 인코딩 시 소문자를 사용하고, 디코딩 시 대소문자를 모두 받아들여요. 인코딩 중 지원 옵션은 없습니다. 디코딩 중 지원 옵션:
- -strict — 공백 문자를 만나면 오류를 던지게 합니다. 그렇지 않으면 무시합니다.
uuencode
uuencode 인코딩은 예전 Unix 시스템 사이와 USENET에서 데이터 전송에 흔히 쓰였지만, 지금은 크게 base64로 대체됐어요. 인코딩 중 지원 옵션(변경 시 다른 디코더 구현이 처리할 수 없는 파일이 나올 수 있음):
- -maxlen length — 인코딩된 각 줄에 만들 최대 문자 수. 유효 범위는 5~85이며, 범위 밖 줄 길이는 인코딩 형식이 수용할 수 없어요. 기본값은 61.
- -wrapchar character — 인코딩된 각 줄의 끝을 표시할 문자(들). { \x09(TAB), \x0B(VT), \x0C(FF), \x0D(CR) } 집합의 문자 0개 이상 뒤에 개행 \x0A(LF) 0~1개가 오는 값만 허용합니다. 다른 값은 디코딩할 수 없는 텍스트를 만들기 때문에 거부돼요. 기본값은 개행 하나.
디코딩 중 지원 옵션:
- -strict — 표준 인코딩 형식 밖의 어떤 것도 만나면 오류를 던지게 합니다. 이 옵션이 없으면 일부 편차를, 대개 인코더-디코더 사이의 줄 재배열을 용납합니다.
인코더도 디코더도 uuencode 형식의 헤더와 푸터를 처리하지 않는다는 점을 기억하세요.
BINARY FORMAT
binary format 명령은 formatString이 지정하는 배치로 이진 문자열을 생성하며, 내용은 추가 인자들에서 옵니다. 결과 이진 값이 리턴됩니다.
formatString은 공백으로 구분된 필드 지정자 0개 이상의 연속으로 이루어져요. 각 필드 지정자는 단일 타입 문자, 선택적 플래그 문자, 선택적 숫자 count로 구성됩니다. 대부분의 필드 지정자는 포맷할 값을 얻기 위해 인자 하나를 소비합니다. 타입 문자는 값이 어떻게 포맷될지를 정해요. count는 보통 값에서 해당 타입의 항목을 몇 개 가져올지를 나타냅니다. 있으면 count는 음이 아닌 십진 정수 또는 *****인데, 보통 값의 모든 항목을 쓰라는 뜻입니다. 인자 수가 인자를 소비하는 형식 문자열의 필드 수와 일치하지 않으면 오류가 발생해요. 플래그 문자는 binary format에서는 무시됩니다.
필드 지정자와 인자의 관계를 명확히 하는 작은 예:
binary format d3d {1.0 2.0 3.0 4.0} 0.1
첫 인자는 네 숫자의 목록인데, 관련 필드 지정자의 count가 3이라 처음 세 개만 쓰입니다. 두 번째 인자는 두 번째 필드 지정자에 연결돼요. 결과 이진 문자열은 1.0, 2.0, 3.0, 0.1 네 숫자를 담습니다.
각 타입-count 쌍은 이진 데이터를 통과하는 가상 커서를 옮깁니다. 커서는 데이터 시작의 위치 0에서 시작해, 현재 위치에 바이트를 저장하고 마지막으로 저장한 바이트 바로 뒤로 이동해요. 타입은 다음 문자 중 하나일 수 있습니다.
- a — 길이 count의 바이트 문자열을 저장. 각 문자는 모듈로 256(즉 각 문자의 하위 바이트 사용, 상위 바이트 폐기)로 취급합니다. 그래서 \u0000-\u00ff로 완전히 표현할 수 없는 문자 문자열을 저장할 때 이 잘림이 싫다면(즉 문자가 ISO 8859-1 문자 집합의 일부가 아니라면) 먼저 encoding convertto 명령으로 문자열을 외부 표현으로 바꿔야 해요. arg가 count 바이트보다 짧으면 추가 0 바이트로 필드를 채웁니다. arg가 지정 길이보다 길면 여분 문자는 무시돼요. count가 *****면 arg의 모든 바이트를 포맷합니다. count가 생략되면 한 문자를 포맷합니다. 예:
binary format a7a*a alpha bravo charlie→ alpha\000\000bravocbinary format a* [encoding convertto utf-8 \u20ac]→ \342\202\254 (유로 화폐 문자의 UTF-8 바이트 시퀀스)binary format a* [encoding convertto iso8859-15 \u20ac]→ \244 (ISO 8859-15 바이트 시퀀스)binary format a* \u20ac→ \254(즉 \xac) — 문자의 상위 비트를 잘라내므로 대개 원하는 결과가 아닙니다.
- A — a와 같지만 패딩에 널 대신 공백을 사용.
binary format A6A*A alpha bravo charlie→ alpha bravoc. - b — 출력 문자열의 각 바이트 내에서 낮은 자리에서 높은 자리 순으로 count개의 이진 숫자 문자열을 저장. arg는 1과 0 문자 문자열이어야 해요. 결과 바이트는 첫 번째에서 마지막 순으로 방출되며 비트는 각 바이트 내에서 낮은 자리→높은 자리 순으로 포맷됩니다. arg가 count보다 적으면 남은 비트는 0으로 채워요. count가 *****면 arg의 모든 자릿수를 포맷합니다. 포맷된 비트 수가 바이트 경계에서 끝나지 않으면 마지막 바이트의 남은 비트는 0이 돼요. 예:
binary format b5b* 11100 111000011010→ \x07\x87\x05. - B — b와 같지만 각 바이트 내에서 높은 자리→낮은 자리 순. 예:
binary format B5B* 11100 111000011010→ \xe0\xe1\xa0. - H — 각 바이트 내에서 높은→낮은 순으로 count개의 16진 숫자 문자열을 저장. arg는 "0123456789abcdefABCDEF" 집합의 문자 문자열이어야 해요. 예:
binary format H3H*H2 ab DEF 987→ \xab\x00\xde\xf0\x98. - h — H와 같지만 각 바이트 내에서 낮은→높은 순. 예:
binary format h3h*h2 AB def 987→ \xba\x00\xed\x0f\x89. - c — 8비트 정수 값 하나 이상을 저장. count 미지정 시 arg는 정수 값 하나여야 하고, count 지정 시 arg는 그만큼 이상의 정수를 담은 목록이어야 해요. 각 정수의 하위 8비트를 커서 위치에 1바이트 값으로 저장합니다. 예:
binary format c3cc* {3 -3 128 1} 260 {2 5}→ \x03\xfd\x80\x04\x02\x05.binary format c {2 5}는 오류를 냅니다. - s — c와 같지만 16비트 정수를 리틀엔디언 바이트 순서로 저장. 가장 덜 중요한 바이트를 먼저 저장해요. 예:
binary format s3 {3 -3 258 1}→ \x03\x00\xfd\xff\x02\x01. - S — s와 같지만 빅엔디언. 예:
binary format S3 {3 -3 258 1}→ \x00\x03\xff\xfd\x01\x02. - t — (기억용: tiny) s·S와 같지만 머신 네이티브 바이트 순서로 저장. 네이티브 바이트 순서는 tcl_platform 배열의 byteOrder 요소를 참조.
- i — c와 같지만 32비트 정수를 리틀엔디언(최하위 바이트 먼저)으로 저장. 예:
binary format i3 {3 -3 65536 1}→ \x03\x00\x00\x00\xfd\xff\xff\xff\x00\x00\x01\x00. - I — i와 같지만 빅엔디언. 예:
binary format I3 {3 -3 65536 1}→ \x00\x00\x00\x03\xff\xff\xff\xfd\x00\x01\x00\x00. - n — (기억용: number/ normal) i·I와 같지만 네이티브 바이트 순서.
- w — c와 같지만 64비트 정수를 리틀엔디언으로 저장. 예:
binary format w 7810179016327718216→ HelloTcl. - W — w와 같지만 빅엔디언. 예:
binary format Wc 4785469626960341345 110→ BigEndian. - m — (기억용: w의 미러) w·W와 같지만 네이티브 바이트 순서.
- f — c와 같지만 단정밀도 부동소수를 머신 네이티브 표현으로 저장. 이 표현은 아키텍처 간 이식성이 없어 네트워크로 부동소수를 통신하는 데 쓰면 안 돼요. 값이 네이티브 표현을 넘치면 시스템이 정의한 FLT_MAX 값이 대신 사용됩니다. Tcl은 내부적으로 배정밀도를 쓰므로 단정밀도 변환 시 약간의 정밀도 손실이 있을 수 있어요. Intel Pentium의 Windows에서
binary format f2 {1.6 3.4}→ \xcd\xcc\xcc\x3f\x9a\x99\x59\x40. - r — (기억용: real) f와 같지만 리틀엔디언으로 저장. IEEE 부동소수 표현을 쓰는(매우 흔하지만 보편적이진 않은) 머신에서만 의미 있는 출력.
- R — r과 같지만 빅엔디언.
- d — f와 같지만 배정밀도 부동소수를 네이티브 표현으로 저장. Intel Pentium의 Windows에서
binary format d1 {1.6}→ \x9a\x99\x99\x99\x99\x99\xf9\x3f. - q — (기억용: d의 미러) d와 같지만 리틀엔디언.
- Q — q와 같지만 빅엔디언.
- x — 출력 문자열에 count개의 널 바이트를 저장. count 미지정 시 널 바이트 하나. count가 *****면 오류. 이 타입은 인자를 소비하지 않아요. 예:
binary format a3xa3x2a3 abc def ghi→ abc\000def\000\000ghi. - X — 출력 문자열에서 커서를 count 바이트 뒤로 이동. count가 *****거나 현재 커서 위치보다 크면 커서를 위치 0에 두어 다음 저장 바이트가 결과 문자열 첫 바이트가 되게 합니다. count 생략 시 커서를 한 바이트 뒤로. 인자를 소비하지 않음. 예:
binary format a3X*a3X2a3 abc def ghi→ dghi. - @ — 커서를 count가 지정한 출력 문자열의 절대 위치로 이동. 위치 0은 출력 문자열 첫 바이트. count가 지금까지 저장된 마지막 바이트 너머를 가리키면 초기화되지 않은 위치에 널 바이트를 두고 커서를 그 위치에 둡니다. count가 *****면 커서를 출력 문자열의 현재 끝으로 이동. count 생략 시 오류. 인자를 소비하지 않음. 예:
binary format a5@2a1@*a3@10a1 abcde f ghi j→ abfdeghi\000\000j.
BINARY SCAN
binary scan 명령은 이진 문자열에서 필드를 파싱하며 수행된 변환 수를 리턴합니다. string은 파싱할 입력 바이트(바이트당 문자 하나, 바이트로 표현할 수 없는 문자는 상위 비트가 잘림)이고 formatString은 파싱 방법을 정해요. 각 varName은 변수 이름을 주는데, string에서 필드가 스캔되면 결과가 해당 변수에 할당됩니다.
binary format과 마찬가지로 formatString은 공백으로 구분된 필드 지정자 0개 이상의 연속입니다. 각 필드 지정자는 단일 타입 문자, 선택적 플래그 문자, 선택적 숫자 count로 구성돼요. 대부분의 필드 지정자는 스캔된 값을 담을 변수를 얻기 위해 인자 하나를 소비합니다. 타입 문자는 이진 데이터가 어떻게 해석될지 정합니다. count는 데이터에서 해당 타입의 항목을 몇 개 가져올지를 나타내며, 있으면 음이 아닌 십진 정수 또는 *****(남은 모든 항목 사용)입니다. 현재 커서 위치 뒤에 현재 필드 지정자를 채울 바이트가 충분하지 않으면 해당 변수는 그대로 두고 binary scan은 설정된 변수 수와 함께 즉시 리턴합니다. 인자를 소비하는 형식 문자열의 모든 필드에 충분한 인자가 없으면 오류가 발생해요. "u" 플래그 문자는 일부 타입을 부호 없는 값으로 읽게 할 수 있습니다. 이 플래그는 모든 필드 타입에 허용되지만 정수가 아닌 필드에서는 무시됩니다.
예로 binary scan $bytes s3s first second는(바이트 문자열이 충분히 길다면) 변수 first에 세 정수 목록을, second에 한 값을 할당합니다. 다음 코드:
puts [binary scan abcdefg s3s first second]
puts $first
puts $second
는 (두 변수가 이전에 설정되지 않았다면) 다음을 출력합니다:
1
25185 25699 26213
can't read "second": no such variable
중요한 점은 c, s, S(그리고 64비트 시스템의 i, I)는 long 데이터 크기 값으로 스캔된다는 것. 그래서 상위 비트가 설정된 값(c는 0x80, short는 0x8000, int는 0x80000000)은 부호 확장됩니다. 예:
set signShort [binary format s1 0x8000]
binary scan $signShort s1 val; # val == 0xFFFF8000
부호 없는 값이 필요하면 필드 타입 뒤에 "u" 플래그 문자를 넣어요. 예:
set signShort [binary format s1 0x8000]
binary scan $signShort su1 val; # val == 0x00008000
각 타입-count 쌍은 이진 데이터를 통과하는 가상 커서를 옮기며 현재 위치에서 바이트를 읽습니다. 스캔 타입 문자는:
- a — 길이 count의 바이트 문자열. count가 *****면 string의 남은 바이트를 모두 변수로 스캔. count 생략 시 한 바이트. 스캔된 모든 바이트는 \u0000-\u00ff 범위의 문자로 해석되므로, 문자열이 이진 문자열이나 ISO 8859-1 인코딩이 아니면 encoding convertfrom이 필요해요. 예:
binary scan abcde\000fghi a6a10 var1 var2→ 1, var1에 abcde\000 저장, var2는 그대로.binary scan \342\202\254 a* var1+set var2 [encoding convertfrom utf-8 $var1]→ var2에 유로 문자가 저장. - A — a와 같지만 변수에 저장하기 전에 뒤쪽 공백과 널을 제거. 예:
binary scan "abc efghi \000" A* var1→ 1, var1에 abc efghi 저장. - b — 데이터를 낮은→높은 순의 count개 이진 숫자 문자열("1"/"0")로 변환. 예:
binary scan \x07\x87\x05 b5b* var1 var2→ 2, var1에 11100, var2에 1110000110100000. - B — b와 같지만 각 바이트 내 높은→낮은 순. 예:
binary scan \x70\x87\x05 B5B* var1 var2→ 2, var1에 01110, var2에 1000011100000101. - H — 데이터를 높은→낮은 순의 count개 16진 숫자 문자열("0123456789abcdef")로 변환. 예:
binary scan \x07\xC6\x05\x1f\x34 H3H* var1 var2→ 2, var1에 07c, var2에 051f34. - h — H와 같지만 각 바이트 내 역순(낮은→높은). 예:
binary scan \x07\x86\x05\x12\x34 h3h* var1 var2→ 2, var1에 706, var2에 502143. 여러 바이트에서 순서대로 16진 숫자를 파싱하려는 코드는 보통 H를 써야 합니다. - c — 데이터를 count개의 8비트 부호 정수로 바꿔 해당 변수에 목록으로 저장. 예:
binary scan \x07\x86\x05 c2c* var1 var2→ 2, var1에 7 -122, var2에 5. 리턴된 정수는 부호 있지만set num [expr { $num & 0xFF }]같은 표현으로 부호 없는 8비트 값으로 바꿀 수 있어요. - s — 데이터를 리틀엔디언 16비트 부호 정수 count개로 해석해 목록으로 저장. 예:
binary scan \x05\x00\x07\x00\xf0\xff s2s* var1 var2→ 2, var1에 5 7, var2에 -16.$num & 0xFFFF로 부호 없는 값 변환 가능. - S — s와 같지만 빅엔디언. 예:
binary scan \x00\x05\x00\x07\xff\xf0 S2S* var1 var2→ 2, 5 7 / -16. - t — s·S와 같지만 머신 네이티브 바이트 순서.
- i — 리틀엔디언 32비트 부호 정수 count개를 목록으로 저장. 예:
binary scan $str i2i* var1 var2→ 2, 5 7 / -16. - I — i와 같지만 빅엔디언. 예:
binary scan $str I2I* var1 var2→ 2, 5 7 / -16. - n — i·I와 같지만 네이티브 바이트 순서.
- w — 리틀엔디언 64비트 부호 정수 count개를 목록으로 저장. 예:
binary scan $str wi* var1 var2→ 2, var1에 30064771077, var2에 -16. 리턴된 정수는 부호 있으며 Tcl로 부호 없는 값으로 표현할 수 없습니다. - W — w와 같지만 빅엔디언. 예:
binary scan $str WI* var1 var2→ 2, 21474836487 / -16. - m — w·W와 같지만 네이티브 바이트 순서.
- f — 데이터를 네이티브 표현의 단정밀도 부동소수 count개로 해석해 목록으로 저장. 데이터가 유효한 부동소수가 아니면 결과 값은 정의되지 않고 컴파일러 의존이에요. Intel Pentium Windows에서
binary scan \x3f\xcc\xcc\xcd f var1→ 1, var1에 1.6000000238418579. - r — f와 같지만 리틀엔디언. IEEE를 쓰지 않는 소수 시스템론 이식 불가.
- R — f와 같지만 빅엔디언.
- d — f와 같지만 배정밀도를 네이티브 표현으로. 예:
binary scan \x9a\x99\x99\x99\x99\x99\xf9\x3f d var1→ 1, 1.6000000000000001. - q — d와 같지만 리틀엔디언.
- Q — d와 같지만 빅엔디언.
- x — string에서 커서를 count 바이트 앞으로 이동. count가 *****거나 현재 커서 뒤의 바이트 수보다 크면 커서를 string의 마지막 바이트 뒤에 둠. count 생략 시 한 바이트 앞으로. 인자를 소비하지 않음. 예:
binary scan \x01\x02\x03\x04 x2H* var1→ 1, var1에 0304. - X — string에서 커서를 count 바이트 뒤로 이동. 예:
binary scan \x01\x02\x03\x04 c2XH* var1 var2→ 2, var1에 1 2, var2에 020304. - @ — 커서를 데이터 문자열의 count가 지정한 절대 위치로 이동. 위치 0은 첫 바이트. count 생략 시 오류. 예:
binary scan \x01\x02\x03\x04 c2@1H* var1 var2→ 2, 1 2 / 020304.
PORTABILITY(이식성) 이슈
r, R, q, Q 변환은 모두 IEEE 부동소수 표현을 쓰는 컴퓨터 사이에서만 안정적으로 동작합니다. 매우 흔하지만 보편적이지는 않아요. 모든 아키텍처 사이에서 부동소수를 이식 가능하게 전송하려면 대신 그 텍스트 표현(format이 만든)을 사용하세요.
예시
길이 워드 뒤에 UTF-8 데이터로 Tcl 문자열을 이진 인코딩된 채널에 쓰는 프로시저:
proc writeString {channel string} {
set data [encoding convertto utf-8 $string]
puts -nonewline [binary format Ia* \
[string length $data] $data]
}
앞의 writeString이 쓴 문자열을 채널에서 읽는 프로시저:
proc readString {channel} {
if {![binary scan [read $channel 4] I length]} {
error "missing length"
}
set data [read $channel $length]
return [encoding convertfrom utf-8 $data]
}
파일(filename 변수) 내용을 base64로 바꿔 출력합니다:
set f [open $filename rb]
set data [read $f]
close $f
puts [binary encode base64 -maxlen 64 $data]
더 알아보기
- encoding, format, scan, string, tcl_platform — 인코딩, 문자열 처리, 플랫폼 정보
- 키워드: binary, format, scan