Goodness, it's covered in tabs!

Space indentation users, unite! We must fight against all the lowly tab users!

Your mission (should you choose to accept it) is to write a program or function that takes in two arguments:

• A string: This is the input.
• A positive integer: This the number of spaces per tab.

You must go through every line of the string and replace every tab used for indentation with the given number of spaces, and every tab not used for indentation (e.g. in the middle of a line) with one space.

Note that lines such as \t \tabc are undefined behavior; they were inserted by the evil tab users to complicate your programs.

According to the Tabs Must Die Society, your program must be as short as possible to avoid detection by the evil tab users.

Example

\t is used to represent tabs here.

Input string:

a
\t\tb\tc
d


Input number:

4


Output:

a
b c
d


The middle line was indented by 8 spaces, 4 per tab (since the given number was 4).

Input string:

\ta\t\tb


Input number:

4


Output:

    a  b


NOTE: This is not a duplicate of the tab expansion challenge; it requires a very different input format and slightly different requirements.

• Yes, as long as the question doesn't explicitly ask for decimal numbers (which it doesn't). Sep 9, 2015 at 21:34
• possible duplicate of Expand tabs (implement expand(1)) Sep 9, 2015 at 22:43
• Proposed test case: \ta\t\tb, 4 (my previous revision was failing that one) Sep 10, 2015 at 4:52
• We need an answer in Whitespace. Sep 11, 2015 at 3:09
• This is truly evil.
– user
Sep 14, 2015 at 14:20

K5, 53 45 bytes

{{,/(b+a&~b:x*&\a:9=y){$[x;x#" ";y]}'y}[x]'y}  In action:  {{,/(b+a&~b:x*&\a:9=y){$[x;x#" ";y]}'y}[x]'y}[4;(,"a";"\t\tb\tc";,"d")]
(,"a"
"        b c"
,"d")


I just want the record to show that this question is morally reprehensible.

• -21346106841 for ... this question is morally reprehensible. Sep 9, 2015 at 21:44
• This needs to end up the top voted answer, simply so people will see the footnote. Sep 9, 2015 at 21:47
• You can return the result instead of printing it for 3 bytes. Sep 9, 2015 at 22:06
• @kirbyfan64sos: I'm printing the result to avoid needing to join the lines. If I can accept and return the result as a list of strings, one for each line, I could save 0: and "\n"\. Sep 9, 2015 at 22:08
• @JohnE I didn't put a rule saying you can't, so I'm guessing you can. :) Sep 9, 2015 at 22:09

Perl, 23 bytes

22 bytes code + 1 bytes command line

Hopefully not too cheeky to assume the numeric input can be given via the -i parameter! Ensure to replace \t in the below code with the actual tab character.

s/\G\t/$"x$^I/ge;y/\t/ /


Usage example:

printf "a\n\t\tb\tc\nd" | perl -p entry.pl -i4


Or for convenience:

printf "a\n\t\tb\tc\nd" | perl -pe 's/\G\t/$"x$^I/ge;y/\t/ /' -i4


Explanation:

Using the -p argument will execute the program for every line in the input, then print the result at the end.

In the above example, the regex substitution replaces \G\t with " "x4 (a space repeated four times). \G is a little-known regex construct which matches either the position of first match if the first iteration, or from the position of the previous match if not the first iteration, meaning it will only replace all tabs at the start of the string, and will do so one-by-one. The y/\t/ / simply replaces all remaining tabs with spaces.

CJam, 3024 23 bytes

q{_9=NA=Seasi*' ?@?:N}/


I usually refuse to post malicious code on the internet…

This is a full program that reads the string from STDIN and the number as a command-line argument.

Try it online in the CJam interpreter.

How it works

q                        Read all input from STDIN.
{                   }/  For each character C in the input:
_9=                      Push 1 if C is a tab and 0 otherwise.
NA=                   See below.
Seasi*             Push a string of W spaces, where W is the integer from
the command-line arguments.
'            Push a spaces character.
?          Select the string if NA= pushed a truthy value, the
single space otherwise.
@         Rotate C on top of the stack.
?        Select the string of spaces or the single space if _9=
pushed 1, the character C otherwise.
:N      Save the result in N.


What NA= does:

• For the first character, N will contain its default value, i.e., the string "\n".

For all subsequent characters, N will contain the result of the last iteration, i.e., the last character from input, a space character or a string of one or more spaces.

• If N is a string, NA= selects the element at index 10 of N (wrapping around). The result will be a space or a linefeed character. Both are truthy.

If N is a character, NA= pushes 1 for a linefeed and 0 otherwise.

• Because of the above, NA= will push a truthy value for the first character, a character preceded by a linefeed or a character preceded by a string of spaces (indentation that has already been replaced).

In all other cases (including a tabulator that has been replace by a space character), NA= will push a falsy value.

• Good thing you're doing the Internet a service by removing malicious tabs. ;) Sep 10, 2015 at 4:03

Julia, 69 59 bytes

f(s,n)=(r=replace)(r(s,r"^\t*"m,i->" "^endof(i)n),"\t"," ")


Ungolfed:

function f(s::String, n::Int)
# Replace the leading indentation tabs
r1 = replace(s, r"^\t*"m, i -> " "^endof(i)n)

# Replace any remaining tabs between words
r2 = replace(r1, r"\t", " ")

# Return
r2
end


Saved 10 bytes and fixed an issue thanks to Glen O!

• Is there any benefit to replacing the leading indentation tabs separately? It seems to me that it should be handled directly by the "remaining tabs" part. Also, your "replace the tabs between text" part will only match a single tab, what if you have "hello\t\t1"? Sep 10, 2015 at 3:25
• If we assume all indentation is done with tabs (no "\t \t" situations), you could do this: f(s,n)=(r=replace)(r(s,r"^\t*"m,i->" "^endof(i)n),"\t"," "), which uses a replacement function and will catch all of the indentation tabs in one hit. Sep 10, 2015 at 3:50
• @GlenO Wow, that's really genius. Thanks so much! Sep 10, 2015 at 3:55
• I noticed my answer got downvoted. Is there anything I've done wrong? I'd be happy to address any issues. Sep 10, 2015 at 17:47
• I don't see any issue. Maybe it's just one of those vindictive types that downvote because they don't like a language, or things like that? I can't see any flaws. Sep 11, 2015 at 2:30

n!('\t':x)=([1..n]>>" ")++n!x
n!x=f<$>x f '\t'=' ' f x=x g n=unlines.map(n!).lines  Then g 3 "a\n\t\tb\tc\nd" does the thing. Mathematica, 42 37 bytes Thanks to @LegionMammal978 for multiple code-saving suggestions. The first parameter, # is for the input text, the second parameter, #2, for the number of spaces per tab. StringReplace[#,"\t"->" "~Table~{#2}]&  • Also, you can change Table[" ",{#2}] to " "~Table~{#2} to save a byte. Why are you StringJoining the empty string onto it? Sep 11, 2015 at 10:50 Ruby 49 bytes def r s,t;s.gsub! /^\t/,' '*t;s.gsub!"\t",' ';end  • This doesn't work if there are two tabs at the beginning of a line. Sep 10, 2015 at 2:24 JavaScript (ES6), 70 Using template strings, the newline is significant and counted (s,n,r=n)=>[...s].map(c=>c< ? .repeat(r):(r=c< ?n:1,c)).join  Test running the snippet below in Firefox. F=(s,n,r=n)=>[...s].map(c=>c< ? .repeat(r):(r=c< ?n:1,c)).join // TEST out=x=>O.innerHTML+=x+'\n\n' out('Input: "A\\n\\t\\tB\\tC\\nD" 4\nOutput:\n'+F('A\n\t\tB\tC\nD',4)) out('Input: "\\tA\\t\\tB" 4\nOutput:\n'+F('\tA\t\tB', 4)) <pre id=O></pre> • Wow one downvote! Could be someone who can't read or understand 'Test in Firefox' ? Sep 10, 2015 at 17:51 • I suspect language bias. Julia and CJam got downvotes as well. Sep 11, 2015 at 20:09 CoffeeScript, 72 bytes (s,n)->s.replace(/^\t+/mg,(m)->" ".repeat(m.length*n)).replace /\t/g," "  (Trying to golf it at least 2 more bites, so it will beat the ES6 solution... Help appreciated :D) Usage: f=(s,n)->s.replace(/^\t+/mg,(m)->" ".repeat(m.length*n)).replace /\t/g," " str=""" My nice\tString \tIndent <-- \t\tDouble """ #\t is recognized as tab by CS alert(f(str,4))  Retina, 42 bytes All occurrences of . are spaces, all \t are literal tabs (1 byte), and <empty> represents a blank file. It's just for readability. I'm also not entirely sure that I'm doing the loop correctly, but I think so. Each line should be placed in its own file. I've added 1 byte for each additional file. Input is assumed to be in Unary on its own line at the end of the input. (1*)$
_$1 m+(?<!^|\t)\t . (1$
<empty>
)\t
\t.
\t|_
<empty>


Explanation

I add a _ before the Unary input to delimit it during replacement, so that I don't remove any trailing ones from the input string. Then, I replace all tabs not at the beginning of a line with a single space. Then, I loop, removing a single 1 and adding a single space after each tab, until I run out of input. Finally, I clean up by removing the tabs and underscore.

Python, 72 68 bytes

Tabs are literal tabs (1 byte), so r'...' is not needed. Unfortunately, Python requires "fixed-width" look-behinds / look-aheads, so I can't use (?<!^|\t). Uses pretty much the same method as my Retina solution.

import re
lambda s,n:re.sub('\t',' '*n,re.sub('(?<!^)(?<!\t)\t',' ',s))


Stax, 20 bytes

ÜR╧█╧╫≡eX,)~σOÜ¢G╩N‼


Run and debug it

This program reads the first line as the indent width, and the rest of the input as the program.

Japt v2.0a0, 17 bytes

r/^\t+/m_çVçÃr\tS


Try it

s#m|let('\t':r)#n=(' '<$[1..n])++r#n;(x:r)#n=x:r#(m^sum[1|x<' ']);e#_=e=s#m  Try it online! This assumes the input contains only printable chars as well as tabs and newlines, as allowed by OP in the comments. Explanation: The outer # function takes a string s and a number m and calls the inner locally defined # function with the same arguments. This is done to keep track of the original m value, as the inner # function changes the number: • ('\t':r)#n=(' '<$[1..n])++r#n If you encounter a tab, replace it by n spaces and leave n unchanged.
• (x:r)#n=x:r#(m^sum[1|x<' ']) If some x which is not a tab is encountered, keep it as is but set n to the original number m if x is a newline and to 1 otherwise. This is done by m^sum[1|x<' ']: m is taken to the power of sum[1|x<' '] which evaluates to 1 when x is smaller than a space (i.e. a newline), so we get m^1 = m. Otherwise it's 0 and we have m^0 = 1.

Java 11, 134 bytes

n->s->{var r="";for(var p:s.split("\n")){for(;p.charAt(0)==9;p=p.substring(1))r+=" ".repeat(n);r+=p+"\n";}return r.replace('\t',' ');}


Try it online.
NOTE: Java 11 isn't on TIO yet, so " ".repeat(n) has been emulated as repeat(" ",n) instead (with the same byte-count).

Explanation:

n->s->{                 // Method with integer & String parameters and String return-type
var r="";             //  Result-String, starting empty
for(var p:s.split("\n")){
//  Loop over the rows (split by new-lines)
for(;p.charAt(0)==9;//   Inner loop as long as the current row starts with a tab
p=p.substring(1))//     After every iteration: remove the first character (tab)
r+=" ".repeat(n); //    Append n amount of spaces to the result-String
r+=p+"\n";}         //   Append the rest of the row with a newline to the result
return r.replace('\t',' ');}
//   Replace any remaining tabs with a space, and return the result