Sed Change Case: Lowercase, Uppercase & Title Case Examples
Master sed change case: convert text to lowercase, uppercase, or title case with \L and \U modifiers, the portable y command, and safe batch examples.
41 min read

Need to change text case with sed? This guide covers every sed change case technique: sed lowercase, sed uppercase, and sed title case, built with the portable y command, GNU sed’s \L/\U modifiers, and \u for single characters. All examples are verified against GNU sed 4.9, with notes for GNU sed 4.10 and macOS/BSD where behavior differs. Useful for cleaning data, normalizing log levels, and batch-fixing file extensions.
The article runs from the basic commands through in-place editing, batch processing, and the traps that make sed lowercase “not work” (it is almost always the y range shorthand or the locale). There is a cheat sheet at the end if you only need the commands.
- GNU sed 4.9+ (or 4.10, released 2026-04-21). Check with
sed –version. macOS/BSD sed works for most examples, with limits noted inline. - A test file to chew on. Never point a first run at data you care about.
- Optional: a git repo around the files you edit.
git checkout – .is the cleanest undo sed will ever get. - Five minutes and one terminal. Everything below is copy-pasteable.
What Is Sed?
sed (Stream Editor) is a command-line tool for text manipulation. It processes input line by line, which makes it fast for large files. If you are still building up your toolkit, it sits alongside grep, awk, and curl in the essential Linux commands cheat sheet.
Sed is great for case conversion because:
- Works non-interactively (no prompts)
- Handles large files efficiently (constant memory, one line at a time)
- Can target specific patterns or line ranges
- Works with pipes and other commands
Other sed guides in this series:
When to Change Text Case with Sed
Common situations:
- Data cleanup - standardize names or fields.
Jon,JON, andjonin the same column is a data-quality bug waiting to bite a join. - Log formatting - make error levels consistent.
ERROR,Error, anderrorin one log file break naive greps and log dashboards. - Config files - some apps demand lowercase keys, others shout everything in caps. Changing case by hand in a 200-line file is how typos happen.
- Batch processing - file extensions, HTTP headers, CSV column headers, docker-compose service names.
Sed is the right tool when the conversion is line-based or pattern-targeted. If you only need to flip an entire stream from upper to lower with no conditions, tr is faster (numbers in the tr vs sed vs awk section). Case conversion is CPU-cheap on small and medium files either way.
Sed Lowercase and Uppercase: The Basic Commands
There are two families of sed case conversion, plus per-character modifiers:
- The
ycommand - portable, works on GNU, BSD, and macOS sed. Character-list based. \Land\Ureplacement modifiers - GNU sed only. Shorter, and they compose with other substitutions.\u,\l,\E- GNU sed only. Control case per character, which is how you build title case.
Most sed change case workflows start with one of the first two.
Convert Case with the y Command (Portable)
y transliterates one set of characters into another. You must list every character. This works everywhere sed works.
Convert to lowercase:
echo "HELLO WORLD" | sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/'
# Output: hello worldConvert to uppercase:
echo "hello world" | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/'
# Output: HELLO WORLDsed y never expands ranges
Unlike tr and unlike regex brackets, the y command does not expand A-Z style ranges. This is the single most common bug in sed case-conversion snippets found online (and it used to live in this article too):
echo "ABC az AZ 09" | sed 'y/A-Z/a-z/'
# Output: aBC az az 09 <- wrong, and silently wrongOnly the literal characters A, -, and Z get transliterated. Always write out the full 26-character lists.
POSIX classes are not valid in y either
y/[[:upper:]]/[[:lower:]]/ looks clever and does nothing useful. The class syntax is not expanded in y, and because both strings happen to be the same length, sed silently transliterates the literal characters (u to l, p to o, e to w, r to e, …) instead of erroring. Use the full lists in y, or [[:upper:]]/[[:lower:]] inside a normal s/// regex where classes do work.
GNU Sed \L and \U: Shorter Case Modifiers
GNU sed adds case modifiers in the replacement part of s///. \L lowercases everything after it, \U uppercases everything after it.
Convert entire line to lowercase (GNU sed):
echo "HELLO WORLD" | sed 's/.*/\L&/'
# Output: hello worldConvert entire line to uppercase (GNU sed):
echo "hello world" | sed 's/.*/\U&/'
# Output: HELLO WORLDTarget specific lines:
sed '/^ERROR/s/.*/\U&/' logfile.txt # uppercases only lines starting with ERRORThese modifiers are GNU-only. They are absent from the FreeBSD and macOS sed man pages, so scripts that must run on a Mac or BSD box should stick to y with full lists. The & in the replacement means “the whole matched text”, which is why s/.*/\L&/ converts the whole line.
GNU sed 4.9 and 4.10
Examples here were verified against GNU sed 4.9 (shipped by Debian 12 / Ubuntu 24.04 class systems, released 2022-11-06). GNU sed 4.10 shipped 2026-04-21, the first stable release in about three and a half years. Relevant changes: s///g no longer panics on lines larger than 2 GB, a TOCTOU race in sed -i –follow-symlinks was fixed (upgrade if you use that flag on shared directories), and –posix now warns about GNU-only backslash escapes in s, which makes it usable as a portability linter for your scripts. Check your version with sed –version.
One-liner on locale: LC_ALL=C sed ... gives predictable ASCII behavior, while \L/\U need a UTF-8 locale to handle non-ASCII text. Details in the gotchas section.
Title Case and Capitalize: \u, \l, and \E
This is the part most “sed case conversion” roundups skip, and it is what the \b[a-z]\+/\U&/g-style snippets get wrong. GNU sed has five replacement case modifiers:
| Modifier | Effect |
|---|---|
\L |
Lowercase everything that follows, until \U or \E |
\l |
Lowercase the next character only |
\U |
Uppercase everything that follows, until \L or \E |
\u |
Uppercase the next character only |
\E |
Stop case conversion started by \L or \U |
Title case every word (each word’s first letter uppercase, rest lowercase):
sed -E 's/[a-z]+/\u&/g' filename.txt
# Input: hello world
# Output: Hello WorldWithout -E (BRE), the same thing:
sed 's/[a-z][a-z]*/\u&/g' filename.txt
# Output: Hello WorldPrefer -E over the older GNU -r spelling for extended regex. -E is the POSIX-standard flag these days (Austin Group bug 528) and is what GNU’s own manual recommends for portability.
Capitalize the first letter of each line:
sed 's/./\u&/' filename.txt
# Input: hello world
# Output: Hello worldUppercase one token and leave the rest of the line alone:
echo "user: admin status: ok" | sed 's/admin/\U&\E/'
# Output: user: ADMIN status: okThe \E leak bug. Case conversion keeps running through the rest of the replacement after a capture group unless you stop it:
echo "host: db1" | sed -E 's/host: (.*)/HOST: \U\1 is up/'
# Output: HOST: DB1 IS UP <- "is up" got uppercased too
echo "host: db1" | sed -E 's/host: (.*)/HOST: \U\1\E is up/'
# Output: HOST: DB1 is up <- \E stops conversion after the groupTwo more subtleties from the manual and from testing:
- With the
gflag, case conversion does not propagate from one match to the next. Each match restarts clean. It does keep running inside one replacement unless\Estops it. - Word boundaries are not portable. GNU sed accepts
\b,\<,\>. Traditional BSD sed wants[[:<:]]and[[:>:]]instead. If a\bexample silently matches nothing, that is usually why (worth confirming on your Mac before trusting either form).
\l and \u touch one character only
s/[[:upper:]]/\l&/g does not lowercase a file. It lowercases just the first character of every uppercase run: HELLO becomes hELLO. For whole-line conversion use s/.*/\L&/ or the full-list y command.
Match Case-Insensitively with the I Flag
Case conversion and case-insensitive matching solve different problems. When you want to find text regardless of case, GNU sed accepts the I flag on regex matches (uppercase i also works):
sed 's/error/CRITICAL/gI' app.log
# matches Error, ERROR, error, and eRRorAddress form, for example deleting every warning line whatever its capitalization:
sed '/^warning/Id' app.logPortability surprise: I is documented as a GNU extension, but FreeBSD and macOS sed also accept i/I as an s flag. That makes it more portable than \L/\U in practice. If you only remember one flag for scripts that travel between Linux and macOS, this is it.
Convert Case in Files: In-Place Editing and Pattern Targeting
Working with files splits into two cases: write the result somewhere else (safe), or edit the file in place (fast, with sharp edges).
Whole file to lowercase (portable):
sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' filename.txt > output.txtWhole file to lowercase (GNU sed):
sed 's/.*/\L&/' filename.txt > output.txtWhole file to uppercase (portable / GNU):
sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' filename.txt > output.txt
sed 's/.*/\U&/' filename.txt > output.txtTarget specific patterns:
sed '/^ERROR/s/.*/\U&/' logfile.txt # only lines starting with ERROR
sed 's/\blinux\b/\U&/g' filename.txt # only the word linux (GNU \b)Practical examples:
sed 's/^./\U&/' filename.txt # uppercase first letter of each line
sed 's/\.[^.]*$/\L&/' filelist.txt # lowercase the file extensionThe extension example matters: s/\.[A-Z]*$/\L&/ only matches extensions that are already all-caps, so photo.Jpg slips through. s/\.[^.]*$/\L&/ matches any extension and lowercases it.
In-place editing
Test the command without -i first. Every time. Then edit in place, and keep a backup on the first pass:
GNU/Linux
# edit in place
sed -i 's/.*/\L&/' filename.txt
# edit in place and keep a backup copy
sed -i.bak 's/.*/\L&/' filename.txt
ls filename.txt.bak # confirm the backup exists before you trust this in a loopmacOS/BSD
# -i REQUIRES a suffix argument on macOS/BSD (empty string = no backup)
sed -i '' 's/.*/\L&/' filename.txt
# cross-platform idiom: GNU treats .bak as a suffix, BSD as a backup extension
sed -i.bak 's/.*/\L&/' filename.txt
ls filename.txt.bakVerify before you move on:
diff original.txt modified.txt
sed -n '1,5p' filename.txtIf the file is in a git repo, skip the scattered .bak clutter and use git commands for undoing changes instead:
sed -i 's/.*/\L&/' filename.txt
git diff --stat # see what actually changed
git checkout -- . # full rollback if the result is wrongSed -i Gotchas: macOS, Inodes, and Docker Bind Mounts
GNU sed -i does not edit your file. It writes the output to a temp file and renames it over the original. The filename stays; the inode changes. That has consequences:
- Hard links break. Anything still pointing at the old inode now holds stale content.
tail -floses the file. It follows the old inode.tail -F(uppercase) reopens by name and survives.- Single-file Docker bind mounts fail. The rename crosses a mount boundary and you get
sed: cannot rename ...: Device or resource busy. Workaround: copy the file out, edit it, copy it back, or mount the containing directory instead of the single file. (Exact error wording is worth a 2-minute check in your own Docker version; see essential Docker commands for the mount basics.) In Alpine/BusyBox containers, sed is BusyBox sed, not GNU sed:-ibehaves slightly differently and\L/\Usupport is not something I would bet a deploy on. - You need free disk. During the rewrite, the temp copy exists alongside the original. Budget roughly 2x the file size.
- Symlink race in older GNU sed.
sed -i --follow-symlinkshad a TOCTOU vulnerability (a symlink swapped between resolution and open) fixed in GNU sed 4.10. If you use that flag on shared directories, upgrade.
macOS and BSD: -i needs a suffix
On macOS and FreeBSD, sed -i without an argument is an error, and sed -i ‘’ ‘…’ means “no backup”. Both BSD man pages warn that a zero-length backup extension risks a corrupted or partial file if the disk fills up mid-write, because there is no backup to fall back to. The safe cross-platform form is sed -i.bak everywhere, then delete the .bak files after you verify.
Batch Case Conversion Across Multiple Files
For loops, find, and xargs cover almost every batch case. Quote your variables and handle spaces in filenames.
Convert all .txt files to lowercase:
for file in *.txt; do
sed -i.bak 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' "$file"
doneLoop syntax differs per shell (word splitting, globbing, set -e behavior). If you script this in anything other than bash, check bash vs zsh vs fish shell differences first.
Recursive, space-safe:
find . -name "*.txt" -type f -print0 | xargs -0 sed -i.bak 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/'Find with -exec:
# one sed invocation per file
find . -name "*.txt" -type f -exec sed -i.bak 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' {} \;
# batched, faster on large trees
find . -name "*.txt" -type f -exec sed -i.bak 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' {} +{} + runs sed on many files per invocation and is the right default. {} \; is easier to read in error output, one file at a time.
Conditional processing:
grep -l "ERROR" *.log | xargs sed -i.bak 's/error/ERROR/g' # only files that contain ERROR
find . -name "*.conf" -exec sed -i.bak '/^#/!s/.*/\L&/' {} \; # lowercase lines that are not commentsPractical examples:
# normalize source files
find ./src -name "*.php" -exec sed -i.bak 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' {} +
# shout every log level in place
find ./logs -name "*.log" -exec sed -i.bak 's/\b\(info\|warn\|error\|debug\)\b/\U&/g' {} +- Backup first.
find . -name “.txt” -exec cp {} {}.backup ; or rely on git. - Test on sample files.
find . -name “sample.txt” -exec sed ‘y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/’ {} ; (no - Use version control or archives before any batch rewrite of tracked files.
- Check results with diff before you delete anything:
diff original.txt modified.txt.
-i on the trial run).Verify Results After Batch Conversion
Batch edits are where silent bugs (like y/A-Z/a-z/) do the most damage. Verify instead of hoping:
# after lowercasing a tree: confirm nothing uppercase remains
grep -rlE '[[:upper:]]' ./converted || echo "clean"
# eyeball the actual changes against the originals
diff -ru ./orig ./converted | head
# in a git repo: summary first, then undo if needed
git diff --stat
git checkout -- .Verify before cleanup
Run the grep, the diff, and git status before deleting any .bak or .backup files. If the grep prints nothing, the diff matches what you intended, and git shows only the expected files, then remove the backups.
Sed Case Conversion Gotchas (and Fixes)
Four failure modes cover nearly every “sed lowercase is not working” report.
1. y/A-Z/a-z/ does nothing useful. y never expands ranges or POSIX classes. Recap from the y command section: use the full 26-character lists, always.
2. Locale collation eats your character classes. [a-z] is not ASCII a through z under every locale. In a Danish locale ^[a-z]$ can match aa (a single collating symbol); Spanish locales do similar things with ll. If a match seems impossible or over-eager:
LC_ALL=C sed 's/[A-Z]/\l&/g' file.txt # deterministic ASCII behavior
sed 's/[[:upper:]]/\l&/g' file.txt # POSIX classes in s/// are locale-aware and explicitLocale is the usual culprit
If a case conversion “works in the terminal but not in cron” (or the other way round), compare locale in both environments. For ASCII-only work, run LC_ALL=C sed …. POSIX classes like [[:upper:]] work inside s/// regexes and are the clearer choice when you mean “all uppercase letters”. They are still not valid in y.
3. Unicode and multibyte text. The old advice “sed is ASCII-only, use perl” is half wrong:
echo "ÉCOLE École naïve" | LC_ALL=C.UTF-8 sed 's/.*/\L&/'
# Output: école école naïve <- GNU \L is locale-aware under a UTF-8 locale
echo "ÉCOLE École naïve" | LC_ALL=C sed 's/.*/\L&/'
# mangled multibyte output <- C locale treats bytes, not characters
echo "ÉCOLE École naïve" | tr '[:upper:]' '[:lower:]'
# Output: École naïve <- tr is byte-oriented and never touches ÉSo: for Unicode, GNU \L/\U under a UTF-8 locale works. tr does not. perl and gawk also work if you already carry them. The y command is list-based and impractical for Unicode.
4. sed -i swaps the inode. Recap from the sed -i gotchas: hard links break, tail -f drops the file, single-file Docker bind mounts can fail on the rename. If the file matters, keep the backup or the git history.
tr vs sed vs awk: Which Tool for Case Conversion?
Quick benchmark on a 77 MB / 2,000,000-line file (GNU sed 4.9, wall time; your numbers will differ, treat these as order of magnitude):
| Command | Time | Notes |
|---|---|---|
tr '[:upper:]' '[:lower:]' < file |
0.05 s | Whole stream, no conditions |
sed 'y/' with the full 26-char lists |
0.26 s | Portable, ~5x tr |
sed 's/.*/\L&/' file |
1.87 s | Convenient, ~37x tr |
Rules of thumb, not dogma:
- Whole-stream flip, nothing else - use
tr. It wins on every multi-GB job. - Line addresses, patterns, or in-place editing - use
sed. - Field or column logic (case depends on which column) - use
awkwithtolower()/toupper(). - Unicode edge cases - GNU
\L/\Uunder a UTF-8 locale, orperl/gawk.
For completeness: GNU sed 4.2.2+ has sed -z, which reads NUL-separated “lines” instead of newline-separated ones. That is the trick for multiline or NUL-delimited input, for example lowercase-casing the values inside a NUL-separated find -print0 stream. And remember the disk note from earlier: in-place -i needs roughly 2x file size in free space while the temp copy exists.
Sed Change Case Cheat Sheet
# lowercase, portable (full lists - never y/A-Z/a-z/)
sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' file.txt
# uppercase, portable
sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' file.txt
# lowercase / uppercase, GNU sed
sed 's/.*/\L&/' file.txt
sed 's/.*/\U&/' file.txt
# title case (first letter of each word), GNU sed
sed -E 's/[a-z]+/\u&/g' file.txt
# capitalize first letter of each line
sed 's/./\u&/' file.txt
# uppercase one token, stop conversion with \E
sed 's/admin/\U&\E/' file.txt
# case-insensitive match, then convert
sed 's/error/CRITICAL/gI' app.log
# in-place with backup (GNU + BSD)
sed -i.bak 's/.*/\L&/' file.txt
# macOS/BSD alternative: sed -i '' 's/.*/\L&/' file.txt
# batch, space-safe
find . -name '*.txt' -type f -exec sed -i.bak 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' {} +
# verify nothing uppercase remains
grep -rlE '[[:upper:]]' . || echo "all lowercase"| Command | Function | Portability |
|---|---|---|
y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ |
Whole-line lowercase | GNU, BSD, macOS |
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/ |
Whole-line uppercase | GNU, BSD, macOS |
s/.*/\L&/ |
Whole-line lowercase | GNU only |
s/.*/\U&/ |
Whole-line uppercase | GNU only |
s/./\u&/ |
Capitalize first char of line | GNU only |
sed -E 's/[a-z]+/\u&/g' |
Title case each word | GNU only |
s/admin/\U&\E/ |
Uppercase one token, keep rest | GNU only |
s/error/ERROR/gI |
Case-insensitive match | GNU + FreeBSD + macOS |
sed -i.bak '…' |
In-place edit with backup | GNU + BSD |
sed -i '' '…' |
In-place edit, no backup | macOS/BSD only |
grep -rlE '[[:upper:]]' |
Verify lowercasing worked | anywhere |
- Test every command without
-ifirst. yneeds full 26-character lists. Ranges do not expand.- Use
\u(not\U) for title case. One letter versus the rest of the string. \Estops case conversion. Without it, conversion leaks past your capture group.- Verify with
grepanddiff, undo withgit checkout.
Sed Change Case FAQ
y or \L/\U - which one should I use?
Use the full-list y command when the script has to run on BSD or macOS sed, or when you want the faster of the two (about 7x faster than \L on large files, per the quick benchmark above). Use \L/\U on GNU sed when you are already inside an s/// substitution or you want the shorter syntax. Never write y/A-Z/a-z/ - it only transliterates the literal characters A, -, and Z.
How do I target specific patterns or lines?
Prefix the substitution with an address. sed '/^ERROR/s/.*/\U&/' file.txt converts only lines starting with ERROR. sed 's/\blinux\b/\U&/g' file.txt converts only the word linux (GNU \b; BSD sed traditionally wants [[:<:]]/[[:>:]]). For case-insensitive matching, add the I flag: sed 's/error/CRITICAL/gI' app.log.
Can I undo sed changes?
Sed has no built-in undo, but you can make every edit reversible. Either keep a backup (sed -i.bak '…' file.txt) or work inside a git repo and roll back with git checkout -- file.txt. The one thing you cannot do is recover an in-place edit from nothing after the fact - so make the backup or the commit before, not after.
How do I handle filenames with spaces?
Use NUL delimiters end to end: find . -name "*.txt" -type f -print0 | xargs -0 sed -i.bak 'command'. In a for loop, quote the variable ("$file") and consider shopt -s nullglob so an empty glob does not pass a literal *.txt to sed.
Does sed handle Unicode case conversion?
The y command does not - it is character-list based and impractical beyond ASCII. GNU sed’s \L/\U does, under a UTF-8 locale: echo "ÉCOLE" | LC_ALL=C.UTF-8 sed 's/.*/\L&/' gives école. Under the C locale the same command mangles multibyte output. tr is byte-oriented and never touches É. For heavier Unicode work, perl or gawk are the usual answer.
How do I capitalize the first letter of each word or line?
For each word (title case), GNU sed: sed -E 's/[a-z]+/\u&/g' file.txt turns hello world into Hello World. For just the first character of each line: sed 's/./\u&/' file.txt turns hello world into Hello world. Remember \u touches one character only - \U would uppercase the whole rest of the line.
Why does my macOS sed -i command fail?
Because BSD sed requires a suffix argument: sed -i 's/a/b/' file is an error, sed -i '' 's/a/b/' file edits without a backup, and sed -i.bak 's/a/b/' file keeps one. The .bak form works on GNU sed too, so use it in portable scripts. And on both platforms, remember -i replaces the file via rename - the inode changes, hard links break, and a single-file Docker bind mount can refuse the rename.


