Sorting seems like a solved problem — everyone knows sort exists — until the actual requirement turns out to be “sort this CSV by the third column, numerically, but keep the header at the top” or “sort these files by modification time” or “sort a list of version numbers correctly instead of alphabetically.” The sort command handles all of this, but its many flags aren’t always intuitive, and a wrapper tool that exposes the common sorting needs clearly is worth having.
This article builds a Bash file sorting tool covering line sorting, field-based sorting, numeric and version-aware sorting, and file-level sorting by metadata.
Step 1: Basic Alphabetical Sorting
sort file.txt
By default, sort orders lines alphabetically (technically, based on the current locale’s collation order). Reversing:
sort -r file.txt
Step 2: Numeric Sorting
Plain alphabetical sort treats numbers as strings, so 10 sorts before 2 (since "1" < "2" character-by-character). Numeric sort fixes this:
sort -n numbers.txt
2
10
100
-n tells sort to interpret each line as a number for comparison purposes, rather than comparing character-by-character.
Step 3: Sorting by a Specific Field/Column
sort -t',' -k3,3n data.csv
-t','sets the field delimiter to a comma.-k3,3restricts the sort key to just the third field (start and end field both3, meaning don’t extend the key beyond that single field).nappended to the key specifier applies numeric comparison to that field specifically.
This is one of the most commonly needed patterns and one of the least obvious from sort --help alone, so it’s worth building directly into a wrapper script.
Step 4: A Wrapper for Common Sort Patterns
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: $0 <file> [-f field_num] [-d delimiter] [-n (numeric)] [-r (reverse)] [-u (unique)] [-H (keep header)]
EOF
exit 1
}
FILE="${1:-}"
[[ -z "$FILE" ]] && usage
shift
FIELD=""
DELIM=","
NUMERIC=false
REVERSE=false
UNIQUE=false
KEEP_HEADER=false
while getopts ":f:d:nruH" opt; do
case "$opt" in
f) FIELD="$OPTARG" ;;
d) DELIM="$OPTARG" ;;
n) NUMERIC=true ;;
r) REVERSE=true ;;
u) UNIQUE=true ;;
H) KEEP_HEADER=true ;;
*) usage ;;
esac
done
[[ -f "$FILE" ]] || { echo "File not found: $FILE"; exit 1; }
SORT_ARGS=()
[[ -n "$FIELD" ]] && SORT_ARGS+=(-t"$DELIM" -k"${FIELD},${FIELD}")
[[ "$NUMERIC" == true ]] && SORT_ARGS+=(-n)
[[ "$REVERSE" == true ]] && SORT_ARGS+=(-r)
[[ "$UNIQUE" == true ]] && SORT_ARGS+=(-u)
if [[ "$KEEP_HEADER" == true ]]; then
HEADER=$(head -n1 "$FILE")
echo "$HEADER"
tail -n +2 "$FILE" | sort "${SORT_ARGS[@]}"
else
sort "${SORT_ARGS[@]}" "$FILE"
fi
Example:
./sort-file.sh sales.csv -f 4 -d ',' -n -r -H
This sorts sales.csv by the fourth comma-separated field, numerically, in reverse (largest first), while keeping the header row pinned at the top untouched.
How the Header-Preservation Logic Works
head -n1 "$FILE"captures just the first line and prints it immediately, before any sorting happens.tail -n +2 "$FILE"outputs everything from line 2 onward (skipping the header), which is then piped intosortwith the accumulated arguments.- This two-step approach is necessary because
sorthas no built-in concept of “header row” — it would happily sort the header into the middle of the data if not explicitly excluded first.
Step 5: Version-Aware Sorting
Sorting version strings like 1.2.10 and 1.2.9 alphabetically gives the wrong order (1.2.10 sorts before 1.2.9, since "1" < "9" character-by-character at that position). GNU sort has a dedicated flag for this:
sort -V versions.txt
1.2.9
1.2.10
1.2.11
2.0.0
-V (version sort) correctly interprets numeric components within strings, which is exactly what’s needed for changelogs, release tags, and file names containing version numbers.
Step 6: Sorting Files by Metadata (Not Content)
Sometimes the goal isn’t sorting lines within a file, but sorting a list of files themselves — by size, modification time, or name:
# By modification time, newest first
ls -t
# By size, largest first
ls -S
# Using find + sort for more control
find . -type f -printf '%T@ %p\n' | sort -rn | cut -d' ' -f2-
The find -printf '%T@ %p\n' pattern prints each file’s modification timestamp (as a Unix epoch number, %T@) followed by its path (%p), one per line. Piping through sort -rn sorts numerically by that leading timestamp, in reverse (newest first), and cut -d' ' -f2- strips the timestamp back off, leaving just the sorted file list. This is more portable and scriptable than relying on ls‘s sort flags, which can behave inconsistently across systems and aren’t meant to be parsed programmatically in the first place.
Step 7: Combining File-Level and Content-Level Sorting
#!/usr/bin/env bash
set -euo pipefail
DIR="$1"
CRITERION="${2:-mtime}"
case "$CRITERION" in
mtime)
find "$DIR" -type f -printf '%T@ %p\n' | sort -rn | cut -d' ' -f2-
;;
size)
find "$DIR" -type f -printf '%s %p\n' | sort -rn | cut -d' ' -f2-
;;
name)
find "$DIR" -type f -printf '%f %p\n' | sort -k1,1 | cut -d' ' -f2-
;;
*)
echo "Unknown criterion: $CRITERION (use mtime|size|name)"
exit 1
;;
esac
./sort-files.sh ./downloads size
This lists every file under ./downloads, sorted largest-first by size, using the file’s byte count (%s) as the sort key.
Real-World Use Cases
- Log analysis: Sort a merged log file chronologically after combining logs from multiple sources with different original orderings.
- CSV reporting: Sort a sales or metrics export by a specific numeric column before generating a summary or chart.
- Release management: Sort Git tags or changelog entries using version-aware sort so
v1.10.0correctly followsv1.9.0. - Disk usage triage: Sort files by size to quickly identify the largest consumers of disk space in a directory needing cleanup.
Automation Example: Weekly Largest-Files Report
#!/usr/bin/env bash
set -euo pipefail
DIR="/var/data"
REPORT="/tmp/largest-files-$(date +%F).txt"
find "$DIR" -type f -printf '%s %p\n' | sort -rn | head -20 | \
awk '{printf "%.2f MB\t%s\n", $1/1024/1024, $2}' > "$REPORT"
echo "Top 20 largest files written to $REPORT"
This finds every file, sorts by size descending, takes the top 20, then reformats the raw byte count into megabytes for readability using awk‘s printf-style formatting.
Best Practices
- Always specify
-nexplicitly when sorting anything numeric — the default string sort silently produces wrong results on numbers without any error to signal the mistake. - Use
-V(version sort) for anything resembling version numbers or numbered sequences embedded in otherwise similar strings. - Preserve headers explicitly when sorting structured data like CSVs — never let a header row get sorted into the data body.
- Prefer
find -printfover parsinglsoutput for file-metadata sorting, sincelsoutput format isn’t guaranteed to be stable or portable across systems.
Security Considerations
- Sorting itself carries minimal direct security risk, but scripts that sort and then act on file lists (e.g., delete the oldest N files) should double- and triple-check the sort criteria and direction before wiring in a destructive action — an inverted sort order in a cleanup script can delete the wrong files entirely.
- When sorting sensitive data (e.g., a CSV of user records) into a new file, ensure the output file’s permissions match or are more restrictive than the original —
sortwriting to a new file doesn’t automatically preserve the source’s permission bits.
Optimization Tips
sorthas a--parallel=Nflag on GNU systems to use multiple threads for large sorts, which can meaningfully speed up sorting very large files.- For huge files that don’t fit comfortably in memory,
sortautomatically falls back to disk-based merge sorting, but setting--buffer-sizeexplicitly can tune this behavior for better performance on systems with ample RAM. - Combine
sort -u(unique) directly instead ofsort | uniqas two separate steps —-uperforms deduplication as part of the same pass, avoiding an extra process and pipe.
Troubleshooting
- Numbers sorting in the wrong order: Almost always a missing
-nflag; default sort is lexicographic (string-based), not numeric. - Locale-dependent sort order surprises: Sort order for text with accented or non-ASCII characters can vary by locale; setting
LC_ALL=Cbeforesortforces a consistent, byte-order-based sort regardless of the system’s configured locale. -kfield sorting seems to sort on the whole line instead of just the field: Confirm the key range syntax is-kN,N(start and end field the same) rather than just-kN, since-kNalone extends the sort key to the end of the line by default.
Common Mistakes
- Forgetting
-nwhen sorting numeric data, leading to"10"sorting before"9". - Sorting a CSV without excluding the header row first, resulting in the header ending up somewhere in the middle of the sorted output.
- Using
-k3instead of-k3,3when only a single field should determine sort order, inadvertently including the rest of the line as a secondary tiebreaker key.
FAQs
How do I sort in a locale-independent, byte-exact way? Prefix the command with LC_ALL=C: LC_ALL=C sort file.txt. This guarantees consistent, portable ordering regardless of the environment’s locale settings.
Can sort handle multiple sort keys (primary, then secondary)? Yes — repeat -k for each key in priority order: sort -k2,2 -k1,1n file.txt sorts primarily by field 2 (as a string), then by field 1 numerically as a tiebreaker.
Is sort -V the same as sort -n? No — -n treats the entire line as one number. -V specifically understands version-style strings with multiple numeric components separated by dots, correctly ordering things like 2.9 before 2.10.
Summary
Sorting is deceptively simple on the surface and full of small correctness traps underneath — string versus numeric comparison, header rows, version strings, and file-metadata sorting all need slightly different handling. A wrapper script that exposes these needs directly (rather than expecting every user to memorize sort‘s flag combinations) turns a frequently-misused command into a reliable, self-documenting tool.