227

I've looked over several questions on Stack Overflow for how to convert spaces to tabs without finding what I need. There seem to be more questions about how to convert tabs to spaces, but I'm trying to do the opposite.

In Vim I've tried :retab and :retab! without luck, but I believe those are actually for going from tabs to spaces anyways.

I tried both expand and unexpand at the command prompt without any luck.

Here is the file in question:

http://gdata-python-client.googlecode.com/hg-history/a9ed9edefd61a0ba0e18c43e448472051821003a/samples/docs/docs_v3_example.py

How can I convert leading spaces to tabs using either Vim or the shell?

brian d foy
  • 129,424
  • 31
  • 207
  • 592
cwd
  • 53,018
  • 53
  • 161
  • 198
  • In @Matt's comment that is now deleted, the first example ( ``sed "s/ +/`echo -e '\t'`/g" < input.py > output.py`` )appears to convert all spaces, not just leading spaces. In the second example (``sed "s/^ +/`echo -e '\t'`/g" < input.py > output.py`` ) it only replaces the first space on each line with a tab and leaves the rest of them. – cwd Feb 01 '12 at 23:24
  • Opposite related: [How to replace tabs with spaces?](http://vi.stackexchange.com/q/495/467) at Vim SE – kenorb Feb 19 '15 at 14:06
  • I didn't find answer for **all/many files**, so I've wrote my own one: https://stackoverflow.com/a/44581474/1115187. With `find`, `awk` and blackjack (too long to leave it in comments, though) – maxkoryukov Jun 16 '17 at 05:54
  • Does this answer your question? [How can I convert tabs to spaces in every file of a directory?](https://stackoverflow.com/questions/11094383/how-can-i-convert-tabs-to-spaces-in-every-file-of-a-directory) – maxkoryukov Jan 11 '20 at 01:53

10 Answers10

380

Using Vim to expand all leading spaces (wider than 'tabstop'), you were right to use retab but first ensure 'expandtab' is reset (:verbose set ts? et? is your friend). retab takes a range, so I usually specify % to mean "the whole file".

:set tabstop=2      " To match the sample file
:set noexpandtab    " Use tabs, not spaces
:%retab!            " Retabulate the whole file

Before doing anything like this (particularly with Python files!), I usually set 'list', so that I can see the whitespace and change.

I have the following mapping in my .vimrc for this:

nnoremap    <F2> :<C-U>setlocal lcs=tab:>-,trail:-,eol:$ list! list? <CR>
johnsyweb
  • 136,902
  • 23
  • 188
  • 247
  • 4
    here's how i got it to work, not sure what is necessary and what is not, and btw i don't know what the "%" before reatab is doing: `:set noet`, `:set tabstop=2`, `:retab!`, `:%retab!`, `:set tabstop=1`, `:retab!`, `:%retab!` – cwd Feb 02 '12 at 01:39
  • 2
    I've updated my answer to include an explanation as to why I use `%`. [The F2 mapping is exactly as it's typed](https://github.com/johnsyweb/dotfiles/blob/master/.vimrc#L324). – johnsyweb Feb 02 '12 at 01:45
  • great, thank you. I was only able to remove all spaces by setting `tabstop` twice (first as 2, then to 1) - does that sound right? thanks for sharing .vimrc – cwd Feb 02 '12 at 01:53
  • 8
    For that particular file, I would just do: `:set noet ts=2 |%retab!` – johnsyweb Feb 02 '12 at 02:15
  • I would add that you need `:set nopreserveindent` as well. – Unk Oct 17 '12 at 10:25
  • @Unk: Quite possibly. I've never set [`'preserveindent'`](http://vimdoc.sourceforge.net/htmldoc/options.html#'preserveindent') and it defaults to `off`. – johnsyweb Oct 17 '12 at 15:51
  • 1
    @Johnsyweb to be fair, I was wrong: `:%retab!` still works. I was confused with `==`, etc which *does* respect the preserveindent setting. – Unk Oct 17 '12 at 21:31
  • 1
    To convert a coffeescript file from spaces to tabs I followed @Johnsyweb's answer (including the addition to the .vimrc file) but instead with a `:set tabstop=4` – Mikeumus Sep 09 '13 at 15:02
  • does this only adjust the leading indentation (i.e. spaces starting at column 0 until the first source code) or does it also convert spaces used for alignment (i.e. any spaces after the first source code)? – Trevor Boyd Smith Oct 03 '17 at 12:18
  • @TrevorBoydSmith: Leading only. – johnsyweb Oct 04 '17 at 02:28
  • I had an issue with all whitespace sections in the document being converted to tabs, how is this better than a plain find replace like `:%s# #\t#g`? – ThorSummoner Feb 07 '18 at 21:18
  • For posterity, setting `list` means issuing `:set list!` which displays normally invisible tab characters as `^I` and normally invisible newline as `$`. To "undo" this command, just reissue `:set list!` (the command behaves like a toggle switch) – Minh Tran Feb 10 '18 at 17:43
49

1 - If you have spaces and want tabs.

First, you need to decide how many spaces will have a single tab. That said, suppose you have lines with leading 4 spaces, or 8... Than you realize you probably want a tab to be 4 spaces. Now with that info, you do:

:set ts=4
:set noet
:%retab!

There is a problem here! This sequence of commands will look for all your text, not only spaces in the begin of the line. That mean a string like: "Hey,␣this␣␣␣␣is␣4␣spaces" will become "Hey,␣this⇥is␣4␣spaces", but its not! its a tab!.

To settle this little problem I recomend a search, instead of retab.

:%s/^\(^I*\)␣␣␣␣/\1^I/g

This search will look in the whole file for any lines starting with whatever number of tabs, followed by 4 spaces, and substitute it for whatever number of tabs it found plus one.

This, unfortunately, will not run at once!

At first, the file will have lines starting with spaces. The search will then convert only the first 4 spaces to a tab, and let the following...

You need to repeat the command. How many times? Until you get a pattern not found. I cannot think of a way to automatize the process yet. But if you do:

`10@:`

You are probably done. This command repeats the last search/replace for 10 times. Its not likely your program will have so many indents. If it has, just repeat again @@.

Now, just to complete the answer. I know you asked for the opposite, but you never know when you need to undo things.

2 - You have tabs and want spaces.

First, decide how many spaces you want your tabs to be converted to. Lets say you want each tab to be 2 spaces. You then do:

:set ts=2
:set et
:%retab!

This would have the same problem with strings. But as its better programming style to not use hard tabs inside strings, you actually are doing a good thing here. If you really need a tab inside a string, use \t.

Community
  • 1
  • 1
DrBeco
  • 11,237
  • 9
  • 59
  • 76
  • Seems a extremely rare race condition. Anyway, create a function accepting visual-range selection and iterate search function until no match found, I guess that would be a more clever and useful answer. – albfan Aug 30 '15 at 09:56
  • Yeas, or that. If you want a more definitive solution you can use a function. Anyway, its always good to know how to do in the `ex command`, because this would be what is inside the function. And no, its not rare. You just need to have strings with spaces to have a mess. Not rare at all. I've being there. Thanks for commenting. – DrBeco Aug 30 '15 at 15:49
  • Please explain why /g is not sufficient to convert all groups of 4 in each line consecutive spaces to tabs, instead of having to run search multiple times. – Bjartur Thorlacius Feb 04 '18 at 20:34
  • Hi @BjarturThorlacius, the problem is that if you remove the `^` symbol, to start the search from the begin of the line, you risk changing spaces inside strings. With the `^` you guarantee you are changing only spaces and tabs since the begin of the line, thus, indentation. Besides that, if you are sure it is ok to do all at once, remove the `^` and run only once with: `:%s/\(^I*\)␣␣␣␣/\1^I/g` – DrBeco Mar 23 '19 at 20:07
21
:%s/\(^\s*\)\@<=    /\t/g

Translation: Search for every instance of 4 consecutive spaces (after the = character), but only if the entire line up to that point is whitespace (this uses the zero-width look-behind assertion, \@<=). Replace each found instance with a tab character.

theEpsilon
  • 1,800
  • 17
  • 30
Simon Zuckerbraun
  • 2,022
  • 1
  • 13
  • 7
  • 1
    The only solution that worked for expanding *1 space* to *1 tab*, but beware the unicode in the answer, I just used `:%s/\(^\s*\)\@<= /\t/g` - put the appropriate number of spaces (to convert 4 spaces to 1 tab, put in 4 spaces) right after the `<=` – Orwellophile Jun 09 '16 at 11:47
8

Linux: with unexpand (and expand)

Here is a very good solution: https://stackoverflow.com/a/11094620/1115187, mostly because it uses *nix-utilities:

  1. unexpand — spaces -> tabs
  2. expand — tabs -> spaces

Linux: custom script

My original answer

Bash snippet for replacing 4-spaces indentation (there are two {4} in script) with tabs in all .py files in the ./app folder (recursively):

find ./app -iname '*.py' -type f \
    -exec awk -i inplace \
    '{ match($0, /^(( {4})*)(.*?)$/, arr); gsub(/ {4}/, "\t", arr[1]) }; { print arr[1] arr[3] }' {} \; 

It doesn't modify 4-spaces in the middle or at the end.

Was tested under Ubuntu 16.0x and Linux Mint 18

maxkoryukov
  • 4,205
  • 5
  • 33
  • 54
8

Changes all spaces to tab :%s/\s/\t/g

Piotr Siupa
  • 3,929
  • 2
  • 29
  • 65
murat budak
  • 81
  • 1
  • 1
1

In my case, I had multiple spaces(fields were separated by one or more space) that I wanted to replace with a tab. The following did it:

:% s/\s\+/\t/g
z atef
  • 7,138
  • 3
  • 55
  • 50
1

at a shell with GNU coreutils: unexpand -t2 --first-only originalFile > newFile explanation: unexpand: the command to turn spaces into tabs -t2: turn 2 spaces into 1 tab --first-only: only modify whitespace at the beginning of a line (including mixed tabs and spaces), do not modify whitespace in the middle or at the end of a line

the reciprocal of this is expand and it takes a simple -i instead of --first-only, with -t2 unchanged

the popular green-checked answer by Johnsyweb is flat out WRONG because it modifies whitespace all over the place!!!

p.s. the crazy formatting of this post does not reflect the quality of the content. it reflects how stackoverflow does not support my browser. thank you.

cubetronic
  • 90
  • 1
  • 9
0

If you have GNU coreutils installed, consider %!unexpand --first-only or for 4-space tabs, consider %!unexpand -t 4 --first-only (--first-only is present just in case you were accidentally invoking unexpand with --all).

Note that this will only replace the spaces preceding the prescribed tab stops, not the spaces that follow them; you will see no visual difference in vim unless you display tabs more literally; for example, my ~/.vimrc contains set list listchars=tab:▸┈ (I suspect this is why you thought unexpand didn't work).

Adam Katz
  • 14,455
  • 5
  • 68
  • 83
0

To use Vim to retab a set of files (e.g. all the *.ts files in a directory hierarchy) from say 2 spaces to 4 spaces you can try this from the command line:

find . -name '*.ts' -print0 | xargs -0 -n1 vim -e '+set ts=2 noet | retab! | set ts=4 et | retab | wq'

What this is doing is using find to pass all the matching files to xargs (the -print0 option on find works with the -0 option to xargs in order to handle files w/ spaces in the name).

xargs runs vim in ex mode (-e) on each file executing the given ex command which is actually several commands, to change the existing leading spaces to tabs, resetting the tab stop and changing the tabs back to spaces and finally saving and exiting.

Running in ex mode prevents this: Vim: Warning: Input is not from a terminal for each file.

Mike Lippert
  • 2,101
  • 1
  • 18
  • 12
-1

Simple Python Script:

import os

SOURCE_ROOT = "ROOT DIRECTORY - WILL CONVERT ALL UNDERNEATH"

for root, dirs, files in os.walk(SOURCE_ROOT):
    for f in files:
        fpath = os.path.join(root,f)
        assert os.path.exists(fpath)
        data = open(fpath, "r").read()
        data = data.replace("    ", "\t")
        outfile = open(fpath, "w")
        outfile.write(data)
        outfile.close()
sudo bangbang
  • 27,127
  • 11
  • 75
  • 77
Michael W.
  • 657
  • 7
  • 5