split [OPTION]... [FILE [PREFIX]]
split -l LINES [FILE [PREFIX]]
split -b SIZE[K|M|G] [FILE [PREFIX]]
split -C SIZE[K|M|G] [FILE [PREFIX]]
The split command divides a large file into smaller, more manageable pieces. By default, it creates output files of 1000 lines each, naming them with alphabetic suffixes (xaa, xab, xac, etc.). This fundamental Unix utility solves a common problem: handling files that are too large to process efficiently, email, transfer, or store as single units.
Split is essential for numerous scenarios: breaking up large log files for easier analysis,
dividing datasets for parallel processing, preparing files for email attachment size limits,
creating manageable backup chunks, splitting data for distributed processing, and working within
filesystem or application size constraints. The split pieces can be easily reassembled using
cat, making split a safe, reversible operation. Modern split implementations support
splitting by line count, byte size, or even by pattern matching, with options for custom naming
schemes and numeric suffixes.
# Create a sample file
seq 1 5000 | awk '{print "Line " $1 ": Sample data"}' > large_file.txt
# Default split (1000 lines per file)
split large_file.txt
# Check what was created
ls -lh x*
# View contents of first chunk
head xaa
# Count lines in each chunk
wc -l x*
# Split into 500-line chunks
split -l 500 large_file.txt chunk_
# Check chunk sizes
wc -l chunk_*
# Split into very small chunks (100 lines)
split -l 100 large_file.txt small_
# Custom split: 250 lines with descriptive prefix
split -l 250 large_file.txt data_part_
# Reassemble the file
cat x* > reassembled.txt
# Verify reassembly
diff large_file.txt reassembled.txt
echo "Files are identical: $?"
# Clean up
rm x* chunk_* small_* data_part_* reassembled.txt
# After default split:
$ ls -lh x*
-rw-r--r-- 1 user user 25K Dec 14 18:00 xaa
-rw-r--r-- 1 user user 25K Dec 14 18:00 xab
-rw-r--r-- 1 user user 25K Dec 14 18:00 xac
-rw-r--r-- 1 user user 25K Dec 14 18:00 xad
-rw-r--r-- 1 user user 25K Dec 14 18:00 xae
$ wc -l x*
1000 xaa
1000 xab
1000 xac
1000 xad
1000 xae
5000 total
# After split -l 500:
$ wc -l chunk_*
500 chunk_aa
500 chunk_ab
500 chunk_ac
500 chunk_ad
500 chunk_ae
500 chunk_af
500 chunk_ag
500 chunk_ah
500 chunk_ai
500 chunk_aj
5000 total
Files are identical: 0
Default suffix is two lowercase letters (aa, ab, ac...). This supports up to 676 chunks (26×26). If you need more chunks, use -a to increase suffix length. The -l option sets lines per chunk. Always verify reassembly with diff or checksums.
# Create a large file (10MB)
dd if=/dev/urandom of=bigfile.bin bs=1M count=10 2>/dev/null
# Split into 1MB chunks
split -b 1M bigfile.bin part_
# Check chunk sizes
ls -lh part_*
# Split into 512KB chunks
split -b 512K bigfile.bin small_part_
# Split into exact byte size (1048576 bytes = 1MB)
split -b 1048576 bigfile.bin byte_part_
# Size with K, M, G suffixes
split -b 2M bigfile.bin big_part_ # 2 megabytes
split -b 100K bigfile.bin tiny_part_ # 100 kilobytes
# Line-boundary-aware size split with -C
# (splits at size but doesn't break lines)
cat > text_data.txt >> 'EOF'
This is a text file with multiple lines.
Each line has varying lengths.
Some lines are short.
Others are considerably longer and contain more information.
EOF
# Repeat to make it larger
for i in {1..1000}; do cat text_data.txt; done > large_text.txt
# Split at 10KB boundaries, preserving whole lines
split -C 10K large_text.txt text_chunk_
# Compare -b vs -C
split -b 10K large_text.txt binary_split_
split -C 10K large_text.txt line_split_
# Check if lines are broken
echo "=== Binary split (may break lines) ==="
tail -3 binary_split_aa | head -1
echo "=== Line-aware split (whole lines only) ==="
tail -1 line_split_aa
# Reassemble and verify binary file
cat part_* > reassembled.bin
cmp bigfile.bin reassembled.bin && echo "Binary files match!"
# Clean up
rm part_* small_part_* byte_part_* big_part_* tiny_part_*
rm text_chunk_* binary_split_* line_split_*
rm bigfile.bin reassembled.bin large_text.txt text_data.txt
# 1MB chunks:
$ ls -lh part_*
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_aa
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ab
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ac
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ad
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ae
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_af
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ag
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ah
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_ai
-rw-r--r-- 1 user user 1.0M Dec 14 18:05 part_aj
# Size suffixes:
K = kilobytes (1024 bytes)
M = megabytes (1024K)
G = gigabytes (1024M)
Binary files match!
Use -b for exact byte splitting (can break lines mid-character). Use -C for line-aware splitting (respects line boundaries, may produce slightly smaller chunks). Size suffixes: K (1024), M (1024K), G (1024M). Binary files work with both -b and -C.
# Create sample file
seq 1 1000 > numbers.txt
# Numeric suffixes with -d
split -d -l 100 numbers.txt num_
# Check naming
ls num_*
# Longer numeric suffix (4 digits) with -a
split -d -a 4 -l 100 numbers.txt part_
# Check naming
ls part_*
# Add custom suffix with --additional-suffix
split -d -l 100 --additional-suffix=.txt numbers.txt chunk_
# Check naming
ls chunk_*
# Combine options: numeric, 3 digits, custom suffix
split -d -a 3 -l 100 --additional-suffix=.log numbers.txt log_
# Check naming
ls log_*
# Create dated output files
DATE=$(date +%Y%m%d)
split -d -l 100 --additional-suffix="_${DATE}.txt" numbers.txt daily_
# Check naming
ls daily_*
# Hexadecimal suffixes with -x
split -x -l 100 numbers.txt hex_
# Check naming (0a, 0b, 0c... 0f, 10, 11...)
ls hex_*
# Complex naming for archive purposes
split -d -a 5 -b 1M --additional-suffix=".tar.gz.part" \
largefile.tar.gz backup_$(date +%Y%m%d)_
# Practical: split log by date in filename
LOG_DATE=$(date +%Y-%m-%d)
split -d -l 1000 --additional-suffix="_${LOG_DATE}.log" \
application.log app_log_part_
# Clean up
rm num_* part_* chunk_* log_* daily_* hex_* app_log_part_*
# Default numeric (-d):
num_00 num_01 num_02 num_03 num_04 num_05 num_06 num_07 num_08 num_09
# 4-digit suffix (-a 4):
part_0000 part_0001 part_0002 part_0003 part_0004 part_0005 ...
# With additional suffix (--additional-suffix=.txt):
chunk_00.txt chunk_01.txt chunk_02.txt chunk_03.txt ...
# Three digits plus .log:
log_000.log log_001.log log_002.log log_003.log ...
# Dated files:
daily_00_20251214.txt daily_01_20251214.txt daily_02_20251214.txt ...
# Hexadecimal suffixes:
hex_00 hex_01 hex_02 hex_03 hex_04 hex_05 hex_06 hex_07 hex_08 hex_09
hex_0a hex_0b hex_0c hex_0d hex_0e hex_0f
The -d flag uses numeric suffixes (00, 01, 02...). The -a N sets suffix length (default 2). The --additional-suffix adds a string after the numeric suffix - great for extensions like .txt or .log. The -x flag creates hexadecimal suffixes. Numeric suffixes are easier to sort and process programmatically.
# Create a simulated large log file
cat > generate_logs.sh >> 'EOF'
#!/bin/bash
for i in {1..10000}; do
timestamp=$(date -d "$i minutes ago" '+%Y-%m-%d %H:%M:%S')
level=$((RANDOM % 4))
case $level in
0) lev="INFO" ;;
1) lev="WARN" ;;
2) lev="ERROR" ;;
3) lev="DEBUG" ;;
esac
echo "$timestamp [$lev] Application message $i"
done
EOF
chmod +x generate_logs.sh
./generate_logs.sh > application.log
# Split into daily-sized chunks (assuming 10000 lines per day)
split -d -l 10000 \
--additional-suffix=.log \
application.log \
app_$(date +%Y%m%d)_
# Split by size (10MB chunks)
split -d -b 10M \
--additional-suffix=.log \
application.log \
app_size_
# Split for archival (1000 lines each, with dates)
split -d -l 1000 \
--additional-suffix="_archive.log" \
application.log \
archive_$(date +%Y%m%d)_
# Split and compress each chunk
split -d -l 5000 application.log split_temp_
for file in split_temp_*; do
gzip -c "$file" > "${file}.log.gz"
rm "$file"
done
# Process each chunk separately (example: count errors)
split -d -l 2000 application.log chunk_
for chunk in chunk_*; do
errors=$(grep -c ERROR "$chunk")
echo "$chunk: $errors errors"
done
# Split logs by severity
grep "ERROR" application.log > errors.log
grep "WARN" application.log > warnings.log
grep "INFO" application.log > info.log
# Then split each severity log
split -d -l 1000 --additional-suffix=.log errors.log error_part_
split -d -l 1000 --additional-suffix=.log warnings.log warn_part_
split -d -l 1000 --additional-suffix=.log info.log info_part_
# Create monthly archive script
cat > archive_logs.sh >> 'EOF'
#!/bin/bash
LOG_FILE="$1"
MONTH=$(date +%Y%m)
# Split into 1GB chunks
split -d -b 1G \
--additional-suffix=".log.part" \
"$LOG_FILE" \
"archive_${MONTH}_"
# Compress each chunk
for chunk in archive_${MONTH}_*.log.part; do
gzip "$chunk"
echo "Compressed: ${chunk}.gz"
done
EOF
chmod +x archive_logs.sh
# Clean up
rm chunk_* split_temp_*.log.gz error_part_* warn_part_* info_part_*
rm errors.log warnings.log info.log
rm generate_logs.sh archive_logs.sh
# Daily chunks:
app_20251214_00.log (10000 lines)
# Size-based chunks:
app_size_00.log (10MB)
app_size_01.log (10MB)
app_size_02.log (remaining)
# Archive chunks:
archive_20251214_00_archive.log (1000 lines)
archive_20251214_01_archive.log (1000 lines)
archive_20251214_02_archive.log (1000 lines)
...
archive_20251214_09_archive.log (1000 lines)
# Error counts per chunk:
chunk_00: 523 errors
chunk_01: 487 errors
chunk_02: 501 errors
chunk_03: 495 errors
chunk_04: 512 errors
# Compressed archives:
Compressed: archive_202512_00.log.part.gz
Compressed: archive_202512_01.log.part.gz
Splitting logs makes them manageable for analysis tools, reduces memory requirements, enables parallel processing, and facilitates archival. Combine split with compression (gzip, xz) to save storage. Use date-based naming for chronological organization. Always keep a backup before splitting critical logs.
# Create a large dataset
seq 1 100000 | awk '{print $1 "," rand()*100 "," rand()*1000}' > data.csv
# Split for parallel processing (10000 lines each)
split -d -l 10000 --additional-suffix=.csv data.csv data_chunk_
# Process chunks in parallel with GNU parallel
if command -v parallel &> /dev/null; then
# Example: sum the second column
parallel "awk -F',' '{sum+=\$2} END {print FILENAME, sum}'" ::: data_chunk_*.csv
fi
# Process chunks in parallel with background jobs
for chunk in data_chunk_*.csv; do
(
# Simulate processing
result=$(awk -F',' '{sum+=$2; count++} END {print sum/count}' "$chunk")
echo "$chunk average: $result"
) &
done
wait
# Split and process with xargs
ls data_chunk_*.csv | \
xargs -n 1 -P 4 -I {} bash -c 'wc -l {} && sleep 1'
# More complex: process and merge results
cat > process_chunk.sh >> 'EOF'
#!/bin/bash
chunk="$1"
output="${chunk}.processed"
# Simulate complex processing
awk -F',' '{
if ($2 > 50) {
print $0 ",HIGH"
} else {
print $0 ",LOW"
}
}' "$chunk" > "$output"
echo "Processed: $chunk -> $output"
EOF
chmod +x process_chunk.sh
# Process all chunks in parallel
for chunk in data_chunk_*.csv; do
./process_chunk.sh "$chunk" &
done
wait
# Merge results
cat data_chunk_*.csv.processed > final_output.csv
echo "Final output lines: $(wc -l < final_output.csv)"
# Split for distributed processing across machines
split -d -l 10000 --additional-suffix=.csv data.csv /tmp/distributed_
# Create processing script for each machine
cat > distributed_process.sh >> 'EOF'
#!/bin/bash
CHUNK="$1"
SERVER="$2"
# Copy to remote server
scp "$CHUNK" "$SERVER:/tmp/"
# Process remotely
ssh "$SERVER" "process_data.sh /tmp/$(basename $CHUNK)"
# Retrieve results
scp "$SERVER:/tmp/$(basename $CHUNK).result" ./results/
EOF
# Example: distribute to 4 servers
# for i in {00..09}; do
# server_num=$((i % 4))
# ./distributed_process.sh distributed_$i.csv server${server_num}.example.com &
# done
# Clean up
rm data_chunk_*.csv data_chunk_*.csv.processed
rm process_chunk.sh final_output.csv
rm /tmp/distributed_*.csv 2>/dev/null
# Parallel processing with background jobs:
data_chunk_00.csv average: 49.87
data_chunk_03.csv average: 50.23
data_chunk_01.csv average: 49.45
data_chunk_02.csv average: 50.67
data_chunk_04.csv average: 49.91
...
# With xargs parallel:
10000 data_chunk_00.csv
10000 data_chunk_01.csv
10000 data_chunk_02.csv
10000 data_chunk_03.csv
...
# Processing chunks:
Processed: data_chunk_00.csv -> data_chunk_00.csv.processed
Processed: data_chunk_01.csv -> data_chunk_01.csv.processed
Processed: data_chunk_02.csv -> data_chunk_02.csv.processed
...
Final output lines: 100000
Split enables parallel processing by dividing work into independent chunks. Use background jobs (&), GNU parallel, or xargs -P for local parallelization. For distributed processing, split files and distribute to multiple machines via scp/rsync. Remember to merge results after processing. Speedup is roughly proportional to number of cores/machines (minus overhead).
# Create a large presentation file (simulated)
dd if=/dev/urandom of=presentation.pptx bs=1M count=30 2>/dev/null
# Check file size
ls -lh presentation.pptx
# Split into 10MB chunks (common email limit)
split -d -b 10M \
--additional-suffix=.part \
presentation.pptx \
presentation_
# List parts
ls -lh presentation_*.part
# Create README for recipient
cat > REASSEMBLE.txt >> 'EOF'
To reassemble the presentation:
On Linux/Mac:
cat presentation_*.part > presentation.pptx
On Windows (Command Prompt):
copy /b presentation_00.part+presentation_01.part+presentation_02.part presentation.pptx
Or use the included script:
bash reassemble.sh
Verify the checksum matches:
MD5: [checksum will be here]
EOF
# Add checksum to README
md5sum presentation.pptx | awk '{print " MD5: " $1}' >> REASSEMBLE.txt
# Create reassembly script
cat > reassemble.sh >> 'EOF'
#!/bin/bash
echo "Reassembling presentation.pptx..."
cat presentation_*.part > presentation.pptx
echo "Done! Verifying checksum..."
# Add verification here
echo "presentation.pptx created successfully"
EOF
chmod +x reassemble.sh
# Split with instructions embedded in filenames
split -d -a 2 -b 10M \
--additional-suffix="_of_3.zip" \
largefile.zip \
archive_part_
# For Windows compatibility, create .bat file
cat > reassemble.bat >> 'EOF'
@echo off
echo Reassembling files...
copy /b presentation_00.part+presentation_01.part+presentation_02.part presentation.pptx
echo Done!
pause
EOF
# Create a complete email package
mkdir email_package
mv presentation_*.part email_package/
mv REASSEMBLE.txt email_package/
mv reassemble.sh email_package/
mv reassemble.bat email_package/
# Create tar archive of the package
tar czf email_package.tar.gz email_package/
echo "Package ready for email: email_package.tar.gz"
echo "Or send individual files from: email_package/"
# Alternative: Base64 encode for text-only email
cat > email_text.sh >> 'EOF'
#!/bin/bash
FILE="$1"
CHUNK_SIZE=10000 # lines, not bytes
# Split file
split -d -l $CHUNK_SIZE \
--additional-suffix=.b64 \
<(base64 "$FILE") \
"${FILE%.${FILE##*.}}_b64_"
echo "Created base64-encoded chunks:"
ls -lh "${FILE%.${FILE##*.}}"_b64_*.b64
echo "To reassemble:"
echo "cat ${FILE%.${FILE##*.}}_b64_*.b64 | base64 -d > $FILE"
EOF
chmod +x email_text.sh
# Clean up
rm -rf email_package email_package.tar.gz
rm presentation.pptx archive_part_* email_text.sh
# Original file:
-rw-r--r-- 1 user user 30M Dec 14 18:30 presentation.pptx
# Split into 10MB chunks:
-rw-r--r-- 1 user user 10M Dec 14 18:30 presentation_00.part
-rw-r--r-- 1 user user 10M Dec 14 18:30 presentation_01.part
-rw-r--r-- 1 user user 10M Dec 14 18:30 presentation_02.part
# Email package contents:
email_package/
├── presentation_00.part
├── presentation_01.part
├── presentation_02.part
├── REASSEMBLE.txt
├── reassemble.sh
└── reassemble.bat
Package ready for email: email_package.tar.gz
Or send individual files from: email_package/
# REASSEMBLE.txt contents:
To reassemble the presentation:
On Linux/Mac:
cat presentation_*.part > presentation.pptx
On Windows (Command Prompt):
copy /b presentation_00.part+presentation_01.part+...
MD5: a1b2c3d4e5f6...
Common email limits: Gmail 25MB, Outlook 20MB, Yahoo 25MB. Split to 10MB to be safe. Always include reassembly instructions for recipients. Include checksums to verify integrity. Create both Unix (cat) and Windows (copy /b) reassembly instructions. Consider base64 encoding for text-only email systems.
# Create sample data
seq 1 10000 > data.txt
# Split and compress each chunk with gzip
split -d -l 1000 \
--filter='gzip > $FILE.gz' \
data.txt chunk_
# Check compressed chunks
ls -lh chunk_*.gz
# Split and compress with xz (better compression)
split -d -l 1000 \
--filter='xz -9 > $FILE.xz' \
data.txt compressed_
# Compare compression
ls -lh compressed_*.xz
# Split and encrypt each chunk
split -d -b 1M \
--filter='openssl enc -aes-256-cbc -salt -pass pass:secret > $FILE.enc' \
sensitive_data.bin encrypted_
# Split with custom processing
split -d -l 500 \
--filter='awk "{print NR, \$0}" | gzip > $FILE.processed.gz' \
data.txt processed_
# Split and upload to remote storage
split -d -b 100M \
--filter='aws s3 cp - s3://bucket/backup/$FILE' \
largefile.tar.gz backup_
# Split with hash generation
split -d -b 10M \
--filter='tee $FILE | md5sum > $FILE.md5' \
archive.tar archive_part_
# Split and send each chunk through pipeline
split -d -l 1000 \
--filter='sort | uniq | gzip > $FILE.sorted.gz' \
unsorted_data.txt sorted_
# Combine multiple filters
split -d -b 5M \
--filter='gzip | gpg --encrypt --recipient user@example.com > $FILE.gpg' \
confidential.pdf secure_
# Split with progress indication
split -d -l 1000 \
--filter='pv -N "Processing $FILE" > $FILE && gzip $FILE' \
largefile.txt monitored_
# Reassemble filtered chunks (gzip example)
for file in chunk_*.gz; do
gunzip -c "$file"
done > reassembled.txt
# Verify reassembly
diff data.txt reassembled.txt
# Reassemble encrypted chunks
for file in encrypted_*.enc; do
openssl enc -d -aes-256-cbc -pass pass:secret -in "$file"
done > decrypted_data.bin
# Clean up
rm chunk_*.gz compressed_*.xz processed_*.processed.gz
rm sorted_*.sorted.gz reassembled.txt
# Compressed chunks:
$ ls -lh chunk_*.gz
-rw-r--r-- 1 user user 2.1K Dec 14 18:45 chunk_00.gz
-rw-r--r-- 1 user user 2.1K Dec 14 18:45 chunk_01.gz
-rw-r--r-- 1 user user 2.1K Dec 14 18:45 chunk_02.gz
...
# Size comparison (1000 lines each):
Original chunk: ~5KB
Gzip compressed: ~2.1KB
XZ compressed: ~1.8KB
# With hash generation:
archive_part_00
archive_part_00.md5
archive_part_01
archive_part_01.md5
# Contents of .md5 file:
a1b2c3d4e5f6g7h8 archive_part_00
# Verification:
Files are identical after reassembly
The --filter option is powerful but use with caution. The $FILE variable contains the chunk filename. Commands in --filter run for each chunk. Test your filter on small data first. Remember that filtered chunks require reverse processing to reassemble (e.g., gunzip for gzipped chunks). Keep encryption passwords secure!
# Create a complete split-and-verify system
cat > smart_split.sh >> 'EOF'
#!/bin/bash
set -euo pipefail
# Configuration
INPUT_FILE="$1"
CHUNK_SIZE="${2:-10M}"
PREFIX="${3:-chunk_}"
# Validation
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: File not found: $INPUT_FILE"
exit 1
fi
echo "Smart Split System"
echo "=================="
echo "Input file: $INPUT_FILE"
echo "Chunk size: $CHUNK_SIZE"
echo "Prefix: $PREFIX"
echo ""
# Generate checksums
echo "Generating checksum..."
ORIGINAL_MD5=$(md5sum "$INPUT_FILE" | awk '{print $1}')
ORIGINAL_SHA256=$(sha256sum "$INPUT_FILE" | awk '{print $1}')
ORIGINAL_SIZE=$(stat -f%z "$INPUT_FILE" 2>/dev/null || stat -c%s "$INPUT_FILE")
echo "Original MD5: $ORIGINAL_MD5"
echo "Original SHA256: $ORIGINAL_SHA256"
echo "Original size: $ORIGINAL_SIZE bytes"
echo ""
# Split the file
echo "Splitting file..."
split -d -b "$CHUNK_SIZE" \
--additional-suffix=.part \
"$INPUT_FILE" \
"$PREFIX"
# Count chunks
CHUNK_COUNT=$(ls ${PREFIX}*.part | wc -l)
echo "Created $CHUNK_COUNT chunks"
echo ""
# Create manifest
MANIFEST="${PREFIX}manifest.txt"
echo "Creating manifest: $MANIFEST"
{
echo "# Split File Manifest"
echo "# Generated: $(date)"
echo "# Original file: $INPUT_FILE"
echo "# Original MD5: $ORIGINAL_MD5"
echo "# Original SHA256: $ORIGINAL_SHA256"
echo "# Original size: $ORIGINAL_SIZE"
echo "# Chunk size: $CHUNK_SIZE"
echo "# Chunk count: $CHUNK_COUNT"
echo ""
echo "# Chunk checksums:"
for chunk in ${PREFIX}*.part; do
chunk_md5=$(md5sum "$chunk" | awk '{print $1}')
chunk_size=$(stat -f%z "$chunk" 2>/dev/null || stat -c%s "$chunk")
echo "$chunk $chunk_size $chunk_md5"
done
} > "$MANIFEST"
# Create reassembly script
REASSEMBLE="${PREFIX}reassemble.sh"
echo "Creating reassembly script: $REASSEMBLE"
cat > "$REASSEMBLE" >> EOFSCRIPT
#!/bin/bash
set -euo pipefail
echo "Reassembly Script"
echo "================="
# Read manifest
MANIFEST="$MANIFEST"
if [ ! -f "\$MANIFEST" ]; then
echo "Error: Manifest not found"
exit 1
fi
# Extract original checksums
ORIGINAL_MD5=\$(grep "# Original MD5:" "\$MANIFEST" | awk '{print \$4}')
ORIGINAL_SIZE=\$(grep "# Original size:" "\$MANIFEST" | awk '{print \$4}')
ORIGINAL_FILE=\$(grep "# Original file:" "\$MANIFEST" | awk '{print \$4}')
echo "Target file: \$ORIGINAL_FILE"
echo "Expected MD5: \$ORIGINAL_MD5"
echo "Expected size: \$ORIGINAL_SIZE bytes"
echo ""
# Verify all chunks exist
echo "Verifying chunks..."
MISSING=0
while read line; do
if [[ "\$line" =~ ^# ]] || [[ -z "\$line" ]]; then
continue
fi
chunk=\$(echo "\$line" | awk '{print \$1}')
if [ ! -f "\$chunk" ]; then
echo "Missing: \$chunk"
MISSING=\$((MISSING + 1))
fi
done < "\$MANIFEST"
if [ \$MISSING -gt 0 ]; then
echo "Error: \$MISSING chunks missing"
exit 1
fi
echo "All chunks present"
echo ""
# Verify chunk checksums
echo "Verifying chunk checksums..."
ERRORS=0
while read line; do
if [[ "\$line" =~ ^# ]] || [[ -z "\$line" ]]; then
continue
fi
chunk=\$(echo "\$line" | awk '{print \$1}')
expected_md5=\$(echo "\$line" | awk '{print \$3}')
actual_md5=\$(md5sum "\$chunk" | awk '{print \$1}')
if [ "\$expected_md5" != "\$actual_md5" ]; then
echo "Checksum mismatch: \$chunk"
ERRORS=\$((ERRORS + 1))
fi
done < "\$MANIFEST"
if [ \$ERRORS -gt 0 ]; then
echo "Error: \$ERRORS chunks have checksum errors"
exit 1
fi
echo "All checksums valid"
echo ""
# Reassemble
echo "Reassembling file..."
cat ${PREFIX}*.part > "\$ORIGINAL_FILE"
# Verify reassembled file
echo "Verifying reassembled file..."
REASSEMBLED_MD5=\$(md5sum "\$ORIGINAL_FILE" | awk '{print \$1}')
REASSEMBLED_SIZE=\$(stat -f%z "\$ORIGINAL_FILE" 2>/dev/null || stat -c%s "\$ORIGINAL_FILE")
if [ "\$REASSEMBLED_MD5" = "\$ORIGINAL_MD5" ]; then
echo "✓ MD5 checksum matches"
else
echo "✗ MD5 checksum MISMATCH"
echo " Expected: \$ORIGINAL_MD5"
echo " Got: \$REASSEMBLED_MD5"
exit 1
fi
if [ "\$REASSEMBLED_SIZE" = "\$ORIGINAL_SIZE" ]; then
echo "✓ File size matches"
else
echo "✗ File size MISMATCH"
exit 1
fi
echo ""
echo "SUCCESS: File reassembled and verified"
echo "Output: \$ORIGINAL_FILE"
EOFSCRIPT
chmod +x "$REASSEMBLE"
echo ""
echo "Split complete!"
echo "Chunks: ${PREFIX}*.part"
echo "Manifest: $MANIFEST"
echo "Reassemble with: ./$REASSEMBLE"
EOF
chmod +x smart_split.sh
# Use the smart split system
dd if=/dev/urandom of=testfile.bin bs=1M count=25 2>/dev/null
./smart_split.sh testfile.bin 5M split_
# Test reassembly
rm testfile.bin
./split_reassemble.sh
# Verify
ls -lh testfile.bin
# Clean up
rm split_*.part split_manifest.txt split_reassemble.sh
rm testfile.bin smart_split.sh
Smart Split System
==================
Input file: testfile.bin
Chunk size: 5M
Prefix: split_
Generating checksum...
Original MD5: a1b2c3d4e5f6...
Original SHA256: 1a2b3c4d5e6f...
Original size: 26214400 bytes
Splitting file...
Created 5 chunks
Creating manifest: split_manifest.txt
Creating reassembly script: split_reassemble.sh
Split complete!
Chunks: split_*.part
Manifest: split_manifest.txt
Reassemble with: ./split_reassemble.sh
# Running reassembly:
Reassembly Script
=================
Target file: testfile.bin
Expected MD5: a1b2c3d4e5f6...
Expected size: 26214400 bytes
Verifying chunks...
All chunks present
Verifying chunk checksums...
All checksums valid
Reassembling file...
Verifying reassembled file...
✓ MD5 checksum matches
✓ File size matches
SUCCESS: File reassembled and verified
Output: testfile.bin
This complete system ensures data integrity through checksums, creates detailed manifests, and provides automated reassembly with verification. Always use checksums for critical data. The manifest file documents the split operation and enables verification. The reassembly script checks every step before declaring success.
# Create multi-document file
cat > documents.txt >> 'EOF'
=== DOCUMENT 1 ===
This is the first document.
It has multiple lines.
More content here.
=== DOCUMENT 2 ===
Second document content.
Different information.
Additional data.
=== DOCUMENT 3 ===
Third document here.
Even more content.
Final lines.
EOF
# Split on pattern using awk
awk '/=== DOCUMENT/{
if (out) close(out)
out = "doc_" sprintf("%02d", ++n) ".txt"
print > out
next
}
out {print > out}' documents.txt
# Check created files
ls -l doc_*.txt
# Split CSV file by column value
cat > data.csv >> 'EOF'
Region,Product,Sales
North,Widget,100
North,Gadget,150
South,Widget,120
South,Gadget,130
East,Widget,110
East,Gadget,140
West,Widget,105
West,Gadget,145
EOF
# Split by region
awk -F',' 'NR==1{header=$0; next}
{
region=$1
if (!(region in seen)) {
print header > region ".csv"
seen[region]=1
}
print > region ".csv"
}' data.csv
# Check created files
ls -l *.csv
# Split log file by date
cat > timestamped.log >> 'EOF'
2025-12-14 Event 1
2025-12-14 Event 2
2025-12-15 Event 3
2025-12-15 Event 4
2025-12-16 Event 5
EOF
awk '{
date=substr($1,1,10)
file="log_" date ".txt"
print > file
}' timestamped.log
# Split on blank lines (paragraphs)
cat > paragraphs.txt >> 'EOF'
First paragraph.
More text here.
Second paragraph.
Different content.
Third paragraph.
Final text.
EOF
awk 'BEGIN{n=1}
/^$/{n++; next}
{print > "para_" sprintf("%02d", n) ".txt"}' paragraphs.txt
# Split XML/JSON by record
cat > records.json >> 'EOF'
{"id": 1, "name": "Alice"}
{"id": 2, "name": "Bob"}
{"id": 3, "name": "Charlie"}
EOF
# Split JSON lines into separate files
awk '{print > "record_" sprintf("%03d", NR) ".json"}' records.json
# Split based on file size threshold within pattern
cat > smart_split_pattern.sh >> 'EOF'
#!/bin/bash
INPUT="$1"
PATTERN="$2"
MAX_SIZE="$3" # in lines
awk -v pattern="$pattern" -v maxsize="$MAX_SIZE" '
BEGIN {chunk=0; count=0}
$0 ~ pattern {
if (count >= maxsize) {
close(out)
chunk++
count=0
}
if (count == 0) {
out = sprintf("chunk_%03d.txt", chunk)
}
}
{
print > out
count++
}' "$INPUT"
EOF
chmod +x smart_split_pattern.sh
# Clean up example files
rm doc_*.txt North.csv South.csv East.csv West.csv
rm log_*.txt para_*.txt record_*.json
rm documents.txt data.csv timestamped.log paragraphs.txt records.json
rm smart_split_pattern.sh
# Documents split:
doc_01.txt doc_02.txt doc_03.txt
# Contents of doc_01.txt:
=== DOCUMENT 1 ===
This is the first document.
It has multiple lines.
More content here.
# CSV files by region:
North.csv South.csv East.csv West.csv
# North.csv contents:
Region,Product,Sales
North,Widget,100
North,Gadget,150
# Log files by date:
log_2025-12-14.txt log_2025-12-15.txt log_2025-12-16.txt
# Paragraphs split:
para_01.txt para_02.txt para_03.txt
# JSON records:
record_001.json record_002.json record_003.json
While split doesn't handle patterns directly, awk provides powerful pattern-based splitting. Use awk to split on delimiters, dates, blank lines, or any pattern you can match. This is essential for structured data like logs, CSV files, or multi-document formats. Combine pattern detection with file size limits for sophisticated splitting.
#!/bin/bash
# File: backup_splitter.sh
# Production backup splitting and archival system
set -euo pipefail
# ============================================
# Configuration
# ============================================
readonly BACKUP_DIR="${BACKUP_DIR:-./backups}"
readonly ARCHIVE_DIR="${ARCHIVE_DIR:-./archives}"
readonly CHUNK_SIZE="${CHUNK_SIZE:-100M}"
readonly COMPRESSION="${COMPRESSION:-gzip}"
readonly LOG_FILE="${LOG_FILE:-backup_split.log}"
# ============================================
# Logging
# ============================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# ============================================
# Initialize directories
# ============================================
mkdir -p "$BACKUP_DIR" "$ARCHIVE_DIR"
# ============================================
# Compression functions
# ============================================
compress_chunk() {
local input="$1"
local output="$2"
case "$COMPRESSION" in
gzip)
gzip -c "$input" > "${output}.gz"
rm "$input"
echo "${output}.gz"
;;
xz)
xz -c "$input" > "${output}.xz"
rm "$input"
echo "${output}.xz"
;;
bzip2)
bzip2 -c "$input" > "${output}.bz2"
rm "$input"
echo "${output}.bz2"
;;
none)
mv "$input" "$output"
echo "$output"
;;
*)
log "Unknown compression: $COMPRESSION"
exit 1
;;
esac
}
# ============================================
# Split and archive function
# ============================================
split_and_archive() {
local source_file="$1"
local backup_name="${2:-$(basename "$source_file")}"
log "========================================="
log "Starting backup split: $backup_name"
log "Source: $source_file"
log "Chunk size: $CHUNK_SIZE"
log "Compression: $COMPRESSION"
log "========================================="
# Validate source
if [ ! -f "$source_file" ]; then
log "Error: Source file not found: $source_file"
return 1
fi
# Create timestamped archive directory
local timestamp=$(date +%Y%m%d_%H%M%S)
local archive_path="$ARCHIVE_DIR/${backup_name}_${timestamp}"
mkdir -p "$archive_path"
log "Archive path: $archive_path"
# Generate checksums
log "Generating checksums..."
local original_md5=$(md5sum "$source_file" | awk '{print $1}')
local original_sha256=$(sha256sum "$source_file" | awk '{print $1}')
local original_size=$(stat -c%s "$source_file" 2>/dev/null || stat -f%z "$source_file")
log "Original MD5: $original_md5"
log "Original size: $original_size bytes"
# Split the file
log "Splitting file..."
local split_prefix="$archive_path/chunk_"
split -d -b "$CHUNK_SIZE" \
-a 4 \
--additional-suffix=.part \
"$source_file" \
"$split_prefix"
# Compress chunks
log "Compressing chunks..."
local chunk_count=0
local compressed_chunks=()
for chunk in ${split_prefix}*.part; do
local compressed=$(compress_chunk "$chunk" "$chunk")
compressed_chunks+=("$compressed")
((chunk_count++))
log " Compressed: $(basename "$compressed")"
done
log "Created $chunk_count chunks"
# Generate manifest
log "Creating manifest..."
local manifest="$archive_path/MANIFEST.txt"
{
echo "# Backup Archive Manifest"
echo "# ======================="
echo "# Created: $(date)"
echo "# Source: $source_file"
echo "# Backup name: $backup_name"
echo "# Original MD5: $original_md5"
echo "# Original SHA256: $original_sha256"
echo "# Original size: $original_size bytes"
echo "# Chunk size: $CHUNK_SIZE"
echo "# Compression: $COMPRESSION"
echo "# Chunk count: $chunk_count"
echo ""
echo "# Chunk details (filename | size | md5):"
for compressed in "${compressed_chunks[@]}"; do
local chunk_size=$(stat -c%s "$compressed" 2>/dev/null || stat -f%z "$compressed")
local chunk_md5=$(md5sum "$compressed" | awk '{print $1}')
local chunk_name=$(basename "$compressed")
echo "$chunk_name | $chunk_size | $chunk_md5"
done
} > "$manifest"
# Create restoration script
log "Creating restoration script..."
local restore_script="$archive_path/RESTORE.sh"
cat > "$restore_script" >> 'EOFSCRIPT'
#!/bin/bash
set -euo pipefail
echo "====================================="
echo "Backup Restoration Script"
echo "====================================="
# Read manifest
MANIFEST="MANIFEST.txt"
if [ ! -f "$MANIFEST" ]; then
echo "Error: MANIFEST.txt not found"
exit 1
fi
# Parse manifest
ORIGINAL_FILE=$(grep "# Source:" "$MANIFEST" | cut -d: -f2- | xargs)
ORIGINAL_MD5=$(grep "# Original MD5:" "$MANIFEST" | awk '{print $4}')
ORIGINAL_SIZE=$(grep "# Original size:" "$MANIFEST" | awk '{print $4}')
COMPRESSION=$(grep "# Compression:" "$MANIFEST" | awk '{print $3}')
CHUNK_COUNT=$(grep "# Chunk count:" "$MANIFEST" | awk '{print $4}')
echo "Source file: $ORIGINAL_FILE"
echo "Expected MD5: $ORIGINAL_MD5"
echo "Expected size: $ORIGINAL_SIZE bytes"
echo "Compression: $COMPRESSION"
echo "Chunks: $CHUNK_COUNT"
echo ""
# Output file
OUTPUT_FILE="${1:-restored_$(basename "$ORIGINAL_FILE")}"
echo "Output file: $OUTPUT_FILE"
echo ""
# Verify chunks
echo "Verifying chunks..."
CHUNK_ERRORS=0
while IFS='|' read -r filename size md5; do
# Skip comments and empty lines
[[ "$filename" =~ ^# ]] && continue
[[ -z "$filename" ]] && continue
# Trim whitespace
filename=$(echo "$filename" | xargs)
size=$(echo "$size" | xargs)
md5=$(echo "$md5" | xargs)
if [ ! -f "$filename" ]; then
echo "✗ Missing: $filename"
((CHUNK_ERRORS++))
else
actual_md5=$(md5sum "$filename" | awk '{print $1}')
if [ "$md5" != "$actual_md5" ]; then
echo "✗ Checksum mismatch: $filename"
((CHUNK_ERRORS++))
fi
fi
done < <(grep -v "^#" "$MANIFEST" | grep -v "^$")
if [ $CHUNK_ERRORS -gt 0 ]; then
echo ""
echo "Error: $CHUNK_ERRORS chunk verification failures"
exit 1
fi
echo "✓ All chunks verified"
echo ""
# Decompress and reassemble
echo "Decompressing and reassembling..."
case "$COMPRESSION" in
gzip)
gunzip -c chunk_*.part.gz | cat > "$OUTPUT_FILE"
;;
xz)
xz -dc chunk_*.part.xz | cat > "$OUTPUT_FILE"
;;
bzip2)
bunzip2 -c chunk_*.part.bz2 | cat > "$OUTPUT_FILE"
;;
none)
cat chunk_*.part > "$OUTPUT_FILE"
;;
*)
echo "Error: Unknown compression: $COMPRESSION"
exit 1
;;
esac
# Verify restored file
echo "Verifying restored file..."
RESTORED_MD5=$(md5sum "$OUTPUT_FILE" | awk '{print $1}')
RESTORED_SIZE=$(stat -c%s "$OUTPUT_FILE" 2>/dev/null || stat -f%z "$OUTPUT_FILE")
if [ "$RESTORED_MD5" = "$ORIGINAL_MD5" ]; then
echo "✓ MD5 checksum matches"
else
echo "✗ MD5 mismatch!"
echo " Expected: $ORIGINAL_MD5"
echo " Got: $RESTORED_MD5"
exit 1
fi
if [ "$RESTORED_SIZE" = "$ORIGINAL_SIZE" ]; then
echo "✓ File size matches"
else
echo "✗ Size mismatch!"
exit 1
fi
echo ""
echo "====================================="
echo "SUCCESS: File restored and verified"
echo "Output: $OUTPUT_FILE"
echo "====================================="
EOFSCRIPT
chmod +x "$restore_script"
# Create archive summary
local total_size=0
for compressed in "${compressed_chunks[@]}"; do
local chunk_size=$(stat -c%s "$compressed" 2>/dev/null || stat -f%z "$compressed")
total_size=$((total_size + chunk_size))
done
local compression_ratio=$(awk "BEGIN {printf \"%.2f\", ($original_size/$total_size)}")
log "========================================="
log "Backup complete!"
log "Original size: $original_size bytes"
log "Compressed size: $total_size bytes"
log "Compression ratio: ${compression_ratio}:1"
log "Chunks: $chunk_count"
log "Archive location: $archive_path"
log "Restore with: cd $archive_path && ./RESTORE.sh"
log "========================================="
}
# ============================================
# Main
# ============================================
main() {
case "${1:-}" in
split)
if [ $# -lt 2 ]; then
echo "Usage: $0 split <file> [backup_name]"
exit 1
fi
split_and_archive "$2" "${3:-}"
;;
*)
echo "Usage: $0 split <file> [backup_name]"
echo ""
echo "Environment variables:"
echo " BACKUP_DIR - Source directory (default: ./backups)"
echo " ARCHIVE_DIR - Archive directory (default: ./archives)"
echo " CHUNK_SIZE - Chunk size (default: 100M)"
echo " COMPRESSION - Compression type: gzip|xz|bzip2|none (default: gzip)"
exit 1
;;
esac
}
main "$@"
$ ./backup_splitter.sh split database.tar
[2025-12-14 19:00:00] =========================================
[2025-12-14 19:00:00] Starting backup split: database.tar
[2025-12-14 19:00:00] Source: database.tar
[2025-12-14 19:00:00] Chunk size: 100M
[2025-12-14 19:00:00] Compression: gzip
[2025-12-14 19:00:00] =========================================
[2025-12-14 19:00:00] Archive path: ./archives/database.tar_20251214_190000
[2025-12-14 19:00:00] Generating checksums...
[2025-12-14 19:00:00] Original MD5: a1b2c3d4...
[2025-12-14 19:00:00] Original size: 524288000 bytes
[2025-12-14 19:00:00] Splitting file...
[2025-12-14 19:00:05] Compressing chunks...
[2025-12-14 19:00:05] Compressed: chunk_0000.part.gz
[2025-12-14 19:00:06] Compressed: chunk_0001.part.gz
[2025-12-14 19:00:07] Compressed: chunk_0002.part.gz
[2025-12-14 19:00:08] Compressed: chunk_0003.part.gz
[2025-12-14 19:00:09] Compressed: chunk_0004.part.gz
[2025-12-14 19:00:09] Created 5 chunks
[2025-12-14 19:00:09] Creating manifest...
[2025-12-14 19:00:09] Creating restoration script...
[2025-12-14 19:00:09] =========================================
[2025-12-14 19:00:09] Backup complete!
[2025-12-14 19:00:09] Original size: 524288000 bytes
[2025-12-14 19:00:09] Compressed size: 367001600 bytes
[2025-12-14 19:00:09] Compression ratio: 1.43:1
[2025-12-14 19:00:09] Chunks: 5
[2025-12-14 19:00:09] Archive location: ./archives/database.tar_20251214_190000
[2025-12-14 19:00:09] Restore with: cd ./archives/database.tar_20251214_190000 && ./RESTORE.sh
[2025-12-14 19:00:09] =========================================
# Archive directory structure:
archives/database.tar_20251214_190000/
├── chunk_0000.part.gz
├── chunk_0001.part.gz
├── chunk_0002.part.gz
├── chunk_0003.part.gz
├── chunk_0004.part.gz
├── MANIFEST.txt
└── RESTORE.sh
This production system provides complete backup splitting with compression, checksums, manifests, and automated restoration. Features: configurable chunk size and compression, integrity verification, detailed logging, timestamped archives, and self-contained restoration. Perfect for automated backup workflows, database dumps, and large file archival.
split -l 1000. Change with -l for lines or -b for bytes.
split -d for better organization. Combine with -a to set suffix length.
cat prefix* > original_file. Wildcard expansion sorts
correctly if you used consistent naming. Verify with diff or checksums after reassembly.
-C SIZE instead of -b SIZE. -C respects line
boundaries, preventing mid-line splits. Chunks may be slightly smaller than specified size.
--additional-suffix=.txt to add extensions for easier identification.
Especially useful for: .part, .log, .csv, or any extension that helps identify file type.
split --filter='gzip > $FILE.gz'.
Saves storage and can be faster than split-then-compress. Remember to decompress when reassembling.
diff original reassembled or compare checksums to ensure perfect reconstruction.