Array#pack
arr.pack(template) -> string Overview
Array#pack turns an array of Ruby values into a binary string by applying a template of single-letter directives. Each directive tells the method how many bytes to write and how to interpret the next array element: unsigned 8-bit integer, little-endian 32-bit float, null-terminated string, and so on. The directive set is shared with String#unpack, and the two methods are almost always used as a pair.
A quick example to anchor the rest of the article:
[104, 105, 33].pack("C*")
# => "hi!"
["Hello"].pack("a*")
# => "Hello"
pack shows up anywhere Ruby needs to talk to the outside world in a compact, well-defined format: writing PNG or WAV headers, building a network packet, encoding a fixed-width record on disk, or producing base64 for an HTTP body. It is one of the few places in core Ruby where you work directly with raw bytes.
Syntax
arr.pack(template)
arr.pack(template, buffer: str)
arr.pack(template) { |packed| ... }
| Parameter | Type | Description |
|---|---|---|
template | String | Format string made of one or more single-letter directives, optionally with a count (* or integer) and an endian modifier (<, >, !, _). Whitespace and # comments are ignored. |
buffer: (keyword) | String | Existing string to append the packed bytes to. Available since Ruby 2.4. |
| Block | — | Yields the packed string before returning it, useful for streaming. |
The return value is a String of binary bytes (ASCII-8BIT encoding). If the template asks for more elements than the array contains, pack raises ArgumentError. If the array has extra elements not covered by the template, those are silently ignored.
# Simplest case: pack every element as a single byte
[10, 20, 30].pack("C*")
# => "\n\x14\x1E"
# Too few elements raises an error
[1].pack("CC")
# => ArgumentError: too few arguments
# Extra elements are silently dropped
[1, 2, 3, 4].pack("C2")
# => "\x01\x02"
Directive Reference
The full directive table is large; this section covers the ones you will reach for most often. For the complete list (which includes M for quoted-printable, P for a pointer to a fixed structure, and the BER-compressed w), see the Packed Data reference.
Integer directives
| Directive | Size | Notes |
|---|---|---|
C / c | 8 bits | Unsigned / signed. Overflow truncates to the low 8 bits. |
S / s | 16 bits | Native byte order. |
L / l | 32 bits | Native byte order. |
Q / q | 64 bits | Native byte order. |
n | 16 bits | Big-endian, unsigned (network byte order). |
N | 32 bits | Big-endian, unsigned. |
v | 16 bits | Little-endian, unsigned. |
V | 32 bits | Little-endian, unsigned. |
U | — | UTF-8 codepoint, one per element. Codepoints above 0x10FFFF raise RangeError. |
The network and little-endian directives (n, N, v, V) are the ones you will use most in practice, because they always produce the same bytes regardless of the host:
# 16-bit big-endian vs little-endian — the bytes are mirrored
[80].pack("n")
# => "\x00P"
[80].pack("v")
# => "P\x00"
# S uses native byte order — output depends on the platform
[80].pack("S")
# => "P\x00" on little-endian, "\x00P" on big-endian
Float directives
| Directive | Precision | Byte order |
|---|---|---|
F / f | single | Native |
D / d | double | Native |
e | single | Little-endian |
E | double | Little-endian |
g | single | Big-endian |
G | double | Big-endian |
If the bytes need to be portable across machines, networks, and file formats, use the explicit-endian directives (e, E, g, G) instead of f or d.
E and G are the safe choices when the bytes need to round-trip across machines:
# Little-endian double — same bytes on every platform
[1.0].pack("E")
# => "\x00\x00\x00\x00\x00\x00\xF0\x3F"
# Big-endian double — same bytes on every platform
[1.0].pack("G")
# => "\x3F\xF0\x00\x00\x00\x00\x00\x00"
# f uses native order — the bytes can differ between x86 and a big-endian CPU
[1.0].pack("f")
String directives
| Directive | Behavior |
|---|---|
a | ASCII/binary bytes, null-padded to the count. |
A | ASCII/binary bytes, space-padded to the count. On unpack, trailing nulls and spaces are stripped. |
Z | Same as a, but Z* adds a null terminator at the end. |
H | Hex string, high nibble first ("DEAD" → 0xDE, 0xAD). |
h | Hex string, low nibble first. |
B | Bit string, MSB first. |
b | Bit string, LSB first. |
m | Base64 (RFC 4648). m0 produces a single line with no newlines. |
u | UU-encoded string. |
Two directives that come up often in real code: m for base64 and Z* for null-terminated strings:
# m produces base64 with a newline at 60 chars (MIME-style)
["hello world"].pack("m")
# => "aGVsbG8gd29ybGQ=\n"
# m0 strips the newline — useful for data: URLs and digest comparisons
["hello world"].pack("m0")
# => "aGVsbG8gd29ybGQ="
# Z* appends a null terminator
["hi"].pack("Z*")
# => "hi\x00"
# a* does NOT append a terminator
["hi"].pack("a*")
# => "hi"
Position directives
| Directive | Effect |
|---|---|
x | Insert a null byte. x4 inserts four. |
X | Back up the write position by one byte. X shrinks the output. |
@ | Jump to an absolute byte offset, null-filling or truncating the gap. |
The position directives are the most error-prone part of the template language. A single typo here produces a string of the wrong length with no exception.
Modifiers
Directives accept four kinds of modifiers:
*: apply to every remaining element. Without*, a directive consumes one element and leaves the rest on the floor.[1, 2, 3].pack("C")returns"\x01";[1, 2, 3].pack("C*")returns"\x01\x02\x03".- Integer count: apply that many times.
[1, 2, 3].pack("C2")packs the first two elements and silently drops the third. - Endianness (
<,>).">L"is big-endian 32-bit unsigned;"<l"is little-endian 32-bit signed. Use these instead of the bare native-order directives when the bytes need to be portable. - Native width (
!,_).i!is a Cint,l!is a Clong,q!is a Clong long. Useful when the on-disk format is defined in terms of C types.
Examples
Pack a network-style header
[4, 1, 80].pack("CCn")
# => "\x04\x01\x00P"
n is 16-bit unsigned big-endian, the byte order used by most network protocols. That makes it the right directive for port numbers, packet lengths, and other fields defined in an RFC.
Pack every remaining element with *
[0, 1, 255, 128].pack("C*")
# => "\x00\x01\xFF\x80"
Without *, pack would encode only the first element and silently drop the rest. The single-element default catches people coming from join or flatten, where every element of the array contributes to the output. The * modifier is what makes pack feel like a vectorized encoder instead of a one-shot formatter, and you will reach for it on almost every real call.
Pack a float in a portable byte order
[Math::PI].pack("E")
# => "\x18\x2D\x44\x54\xFB\x21\x09\x40"
E is a little-endian double, so the bytes are the same on every platform. Compare with f (native order), whose output differs between x86 and big-endian hardware.
Convert a hex string to raw bytes
["DeadBeef"].pack("H*")
# => "\xDE\xAD\xBE\xEF"
["DeadBeef"].pack("h*")
# => "\xED\xDA\xEB\xFE"
H reads hex pairs as written. h swaps the nibbles inside each byte, so the same input produces a different result. Use H* when the source is a typical hex dump from xxd or a debugger.
Pack UTF-8 codepoints
[0x41, 0xE9, 0x1F600].pack("U*")
# => "Aé\u{1F600}"
Each element is treated as an integer codepoint and encoded as UTF-8. The elements must be Integers, not single-character Strings. Packing a character like 'A' instead of the codepoint 0x41 raises TypeError, because U expects an integer in the valid Unicode range. If you already have a string and want to confirm it is well-formed UTF-8 before packing, run String#valid_encoding? on it first.
Append to an existing buffer
prefix = "HDR:"
[1, 2, 3].pack("C*", buffer: prefix)
# => "HDR:\x01\x02\x03"
When you already have a header or framing string in hand, buffer: writes straight into it instead of allocating a fresh result. Useful for building binary records in a tight loop.
Use position directives to leave a hole in the output
[1, 2].pack("C@5C")
# => "\x01\x00\x00\x00\x00\x02"
# 1 (byte 0), null-fill bytes 1-4, then 2 (byte 5)
[0, 1, 2].pack("CCXC")
# => "\x00\x02"
# Write 0, write 1, X shrinks the buffer by 1 (last byte dropped), write 2
@ jumps to an absolute offset; X backs up the write position; x writes a null byte. A single typo between them produces a string of the wrong length with no exception.
Round-trip with String#unpack
packed = [4, 1, 80].pack("CCn")
packed.unpack("CCn")
# => [4, 1, 80]
pack and unpack use the same template grammar. If you can pack something, you can unpack it back to the original array.
Common Mistakes
Out-of-range integers truncate silently. [257].pack("C") returns "\x01", not "\x02\x01". C is 8 bits, so only the low 8 bits survive. If you need an error, validate the value yourself before packing.
More elements than directives drops data. [1, 2, 3].pack("C2") packs the first two and ignores the third, with no warning. Conversely, [1].pack("CC") raises ArgumentError because the second C has nothing to consume.
Native byte order is not portable. s, S, l, L, q, Q use the host’s endianness. The same code produces different bytes on x86 and on big-endian hardware. For anything that crosses a process or network boundary, use n, N, v, V, or the > / < modifiers.
A and a are not interchangeable. A* strips trailing nulls and spaces on unpack; a* preserves them. If you write a C-style fixed-width record with A* (space-padded) and read it back with a*, the trailing padding ends up inside the string. If you write with a* (null-padded) and read with A*, the nulls are silently dropped. Pick one and use it on both sides.
m adds newlines by default. The default m count inserts a newline every 60 characters (rounded down to a multiple of 3) for MIME compatibility. If you need raw base64, for example to embed in a data: URL or to compare against a SHA, use m0.
Treat the template as untrusted input. pack parses the template at runtime, and a malformed or attacker-controlled template can trigger edge cases in the parser. Pass templates you wrote yourself, or validate them before use.
See Also
String#unpack: the direct inverse, and it uses the same template grammar.String#bytes: work with raw bytes, one at a time.- Ruby String Manipulation: broader context for string work in Ruby.