Convert Leading Spaces to Tabs Safely with unexpand
unexpand converts runs of spaces to tab characters and writes the result to standard output, leaving your original file untouched. This guide uses the GNU coreutils 9.4 unexpand installed on this machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about five minutes for a one-off conversion, longer if you need to choose and check non-default tab stops. The examples use ordinary user permissions; no command here needs sudo.
1. Check the installed command
Confirm which executable your shell will run and record its version:
$ command -v unexpand
/usr/bin/unexpand
$ unexpand --version
unexpand (GNU coreutils) 9.4
The version line can carry extra copyright and licence text. What matters is that the command is GNU coreutils 9.4, or another version whose local manual matches the options you intend to use.
Checkpoint
If command -v prints nothing, stop here. The package is not available on your current PATH; do not guess at a replacement command.
2. Convert leading spaces from a file
By default, unexpand converts blanks at the start of each line: it reads the named file and writes converted text to standard output.
$ printf ' first item\n second item\nplain text\n' > sample.txt
$ unexpand sample.txt > sample-tabs.txt
$ sed -n 'l' sample-tabs.txt
\tfirst item$
\tsecond item$
plain text$
The sed -n 'l' display makes a tab visible as \t. A line with eight leading spaces becomes one tab, using the default tab spacing of eight columns. Four leading spaces do not become a tab in this example, because they do not reach the next tab stop.
The input stays untouched. The output file is new, so you can compare both versions before replacing anything:
$ cmp -s sample.txt sample-tabs.txt; printf 'files differ: %s\n' "$?"
files differ: 1
$ sed -n 'l' sample.txt
first item$
second item$
plain text$
Checkpoint
Verify the visible tab markers and keep the original file until the converted one has passed your own checks.
3. Convert spaces throughout each line
Without an option, interior runs of spaces are left alone. Add --all when spaces after the first non-blank character also need converting:
$ printf 'name value\n indented value\n' | unexpand --all | sed -n 'l'
name\tvalue$
\tindented\tvalue$
This changes every eligible blank run according to the tab stops. It will not turn every individual space into a tab, because a tab must advance to a tab position, so a short run can stay as spaces.
--first-only is the explicit way to request leading runs only, and it overrides --all, which matters when a shared command line or script might set both:
$ printf 'name value\n indented value\n' | unexpand --all --first-only | sed -n 'l'
name value$
\tindented value$
Pick one clear mode per script. The default and --first-only are the safer choices when spaces inside prose, data or quoted text carry meaning.
4. Choose a tab width or explicit stops
The default tab stops are eight characters apart. Use --tabs=N for a regular spacing, and remember it also enables conversion of all eligible blanks:
$ printf ' four-space value\n' | unexpand --tabs=4 | sed -n 'l'
\tfour-space value$
For a document with irregular layout, pass a comma-separated list of tab positions. This one sets stops at columns 4, 8 and 12:
$ printf ' value\n' | unexpand --tabs=4,8,12 | sed -n 'l'
\t\t\tvalue$
- Positions are columns, not counts. They describe where tabs stop, not how many spaces to replace everywhere.
- A trailing
/sets the continuing tab size. Prefix the last listed position with it. - A leading
+makes continuing stops relative. They then count from the last explicit position instead of column one.
Use these forms only when the target format documents the layout it actually needs.
Checkpoint
Inspect a representative line after changing tab stops. Terminals and editors can render tabs at different widths, so a conversion that looks aligned in one place may not look aligned in another.
5. Replace a file only after checking it
Shell redirection with > truncates its destination before unexpand even starts, which can destroy a useful file if you accidentally reuse its name. Write to a temporary sibling, inspect it, then move it into place:
$ unexpand --all input.txt > input.txt.new
$ sed -n '1,20l' input.txt.new
$ cmp -s input.txt input.txt.new; printf 'comparison status: %s\n' "$?"
comparison status: 1
$ mv input.txt.new input.txt
Warning
That final mv replaces the original name. Keep a backup first if the input is valuable:
$ cp --preserve=all input.txt input.txt.bak
$ unexpand --all input.txt > input.txt.new
$ mv input.txt.new input.txt
To undo the replacement, restore the backup once you have checked it is the right file:
$ mv input.txt.bak input.txt
Do not remove the backup until you have opened or otherwise validated the replacement. Deleting it with rm is irreversible through this workflow.
6. Handle standard input and failures
With no file argument, or with -, unexpand reads standard input, which makes it useful in pipelines:
$ printf ' one\n two\n' | unexpand | sed -n 'l'
\tone$
\ttwo$
$ unexpand - < input.txt > input-tabs.txt
A non-zero exit status means the command did not finish successfully. Check the input path, permissions and option spelling before changing anything:
$ test -r input.txt && echo 'input is readable'
input is readable
$ unexpand --help | sed -n '1,16p'
Treat malformed tab specifications or an unreadable file as errors. Do not hide them behind a pipeline that ignores the final status; in a shell script, enable a suitable error policy or check $? immediately after the conversion.
Done means
- Confirmed the version. You know the installed GNU coreutils version and command path.
- Chose deliberately. You picked leading-only conversion or
--allon purpose, not by default. - Checked tab visibility. A command such as
sed -n 'l'confirmed the tabs actually landed. - Set stops correctly. The tab stops you chose match the format or editor that will consume the file.
- Kept a backup. You wrote to a new file first and retained a backup before replacing valuable data.
- Know the input rules. Standard input is used when no file, or
-, is supplied.