275

如何将目录的每个文件中的制表符转换为空格(可能是递归的)?

另外,有没有办法设置每个选项卡的空格数?

4

19 回答 19

369

简单的替换sed是可以的,但不是最好的解决方案。如果选项卡之间有“额外”空格,它们在替换后仍然存在,因此边距将参差不齐。在行中间展开的选项卡也将无法正常工作。在bash中,我们可以改为

find . -name '*.java' ! -type d -exec bash -c 'expand -t 4 "$0" > /tmp/e && mv /tmp/e "$0"' {} \;

应用于expand当前目录树中的每个 Java 文件。-name如果您针对某些其他文件类型,请删除/替换参数。-name正如其中一条评论所提到的,在删除或使用弱通配符时要非常小心。您可以无意识地轻松破坏存储库和其他隐藏文件。这就是为什么原始答案包括以下内容的原因:

在尝试这样的事情之前,您应该始终制作树的备份副本,以防出现问题。

于 2012-06-19T04:58:54.800 回答
208

试试命令行工具expand

expand -i -t 4 input | sponge output

在哪里

  • -i用于仅展开每行上的前导制表符;
  • -t 4意味着每个选项卡将被转换为 4 个空白字符(默认为 8 个)。
  • sponge来自moreutils包,并避免清除输入文件。在 macOS 上,该软件包moreutils可通过Homebrew ( brew install moreutils) 或MacPorts ( sudo port install moreutils) 获得。

最后,您可以在使用Homebrew ( ) 或MacPorts ( )gexpand安装后在 macOS 上使用。coreutilsbrew install coreutilssudo port install coreutils

于 2012-06-19T04:33:49.653 回答
72

警告:这会破坏你的回购。

将损坏二进制文件,包括svn, .git!使用前请阅读评论!

find . -iname '*.java' -type f -exec sed -i.orig 's/\t/ /g' {} +

原始文件另存为[filename].orig.

将“*.java”替换为您要查找的文件类型的文件结尾。这样可以防止二进制文件意外损坏。

缺点:

  • 将替换文件中所有位置的选项卡。
  • 如果您碰巧在此目录中有 5GB 的 SQL 转储,将需要很长时间。
于 2012-06-19T04:35:01.260 回答
43

从Gene 的答案中收集最好的评论,迄今为止最好的解决方案是使用spongefrom moreutils

sudo apt-get install moreutils
# The complete one-liner:
find ./ -iname '*.java' -type f -exec bash -c 'expand -t 4 "$0" | sponge "$0"' {} \;

解释:

  • ./正在从当前目录递归搜索
  • -iname是不区分大小写的匹配项(对于两者*.java*.JAVA喜欢)
  • type -f仅查找常规文件(无目录、二进制文件或符号链接)
  • -exec bash -c在子shell中为每个文件名执行以下命令,{}
  • expand -t 4将所有 TAB 扩展为 4 个空格
  • sponge吸收标准输入(来自expand)并写入文件(同一个)*。

注意:* 简单的文件重定向 ( > "$0") 在这里不起作用,因为它会很快覆盖文件

优点:保留所有原始文件权限,不tmp使用中间文件。

于 2017-04-20T15:08:29.040 回答
22

使用反斜杠转义sed

在 Linux 上:

  • 在所有 *.txt 文件中将所有选项卡替换为 1 个连字符:

    sed -i $'s/\t/-/g' *.txt
    
  • 在所有 *.txt 文件中将所有选项卡替换为 1 个空格:

    sed -i $'s/\t/ /g' *.txt
    
  • 在所有 *.txt 文件中将所有制表符替换为 4 个空格:

    sed -i $'s/\t/    /g' *.txt
    

在 Mac 上:

  • 在所有 *.txt 文件中将所有制表符替换为 4 个空格:

    sed -i '' $'s/\t/    /g' *.txt
    
于 2015-10-29T22:33:13.293 回答
10

您可以使用普遍可用的pr命令(此处的手册页)。例如,要将制表符转换为四个空格,请执行以下操作:

pr -t -e=4 file > file.expanded
  • -t抑制标题
  • -e=num将制表符扩展到num空格

递归转换目录树中的所有文件,同时跳过二进制文件:

#!/bin/bash
num=4
shopt -s globstar nullglob
for f in **/*; do
  [[ -f "$f" ]]   || continue # skip if not a regular file
  ! grep -qI "$f" && continue # skip binary files
  pr -t -e=$num "$f" > "$f.expanded.$$" && mv "$f.expanded.$$" "$f"
done

跳过二进制文件的逻辑来自这篇文章

笔记:

  1. 在 git 或 svn 存储库中这样做可能很危险
  2. 如果您的代码文件在字符串文字中嵌入了裸标签,这不是正确的解决方案
于 2017-05-04T05:47:40.190 回答
5

我喜欢上面递归应用程序的“查找”示例。为了使其成为非递归的,仅更改当前目录中匹配通配符的文件,shell glob 扩展对于少量文件就足够了:

ls *.java | awk '{print "expand -t 4 ", $0, " > /tmp/e; mv /tmp/e ", $0}' | sh -v

如果您希望它在您相信它可以工作后保持沉默,只需在最后删除-v命令sh即可。

当然,您可以在第一个命令中选择任何一组文件。例如,以受控方式仅列出特定的子目录(或目录),如下所示:

ls mod/*/*.php | awk '{print "expand -t 4 ", $0, " > /tmp/e; mv /tmp/e ", $0}' | sh

或者依次运行 find(1) 与深度参数等的某种组合:

find mod/ -name '*.php' -mindepth 1 -maxdepth 2 | awk '{print "expand -t 4 ", $0, " > /tmp/e; mv /tmp/e ", $0}' | sh
于 2014-04-29T15:31:17.957 回答
5

您可以为此使用findwith tabs-to-spacespackage。

一、安装tabs-to-spaces

npm install -g tabs-to-spaces

然后,从项目的根目录运行此命令;

find . -name '*' -exec t2s --spaces 2 {} \;

这会将每个文件中的每个tab字符替换为 2 spaces

于 2017-01-12T09:08:50.537 回答
5

如何将目录的每个文件中的制表符转换为空格(可能是递归的)?

这通常不是你想要的。

您想为 png 图像执行此操作吗?PDF文件?.git 目录?你的 Makefile需要标签)?一个 5GB 的 SQL 转储?

从理论上讲,您可以将大量排除选项传递给find您正在使用的任何其他选项;但这很脆弱,一旦添加其他二进制文件就会中断。

你想要的,至少是:

  1. 跳过特定大小的文件。
  2. 通过检查是否存在 NULL 字节来检测文件是否为二进制文件。
  3. 仅替换文件开头expand的制表符(这样做,sed 不这样做)。

据我所知,没有“标准”的 Unix 实用程序可以做到这一点,而且使用 shell 单行也不是很容易,所以需要一个脚本。

不久前,我创建了一个名为 sanitize_files的小脚本,它正是这样做的。它还修复了一些其他常见的东西,比如替换\r\n\n、添加尾随\n等。

您可以在下面找到一个没有额外功能和命令行参数的简化脚本,但我建议您使用上面的脚本,因为它更有可能收到错误修复和其他更新而不是这篇文章。

我还想指出,作为对这里的其他一些答案的回应,使用 shell globbing并不是一种可靠的方法,因为迟早你会得到比适合更多的文件ARG_MAX(在现代Linux 系统是 128k,可能看起来很多,但迟早是不够 的)。


#!/usr/bin/env python
#
# http://code.arp242.net/sanitize_files
#

import os, re, sys


def is_binary(data):
    return data.find(b'\000') >= 0


def should_ignore(path):
    keep = [
        # VCS systems
        '.git/', '.hg/' '.svn/' 'CVS/',

        # These files have significant whitespace/tabs, and cannot be edited
        # safely
        # TODO: there are probably more of these files..
        'Makefile', 'BSDmakefile', 'GNUmakefile', 'Gemfile.lock'
    ]

    for k in keep:
        if '/%s' % k in path:
            return True
    return False


def run(files):
    indent_find = b'\t'
    indent_replace = b'    ' * indent_width

    for f in files:
        if should_ignore(f):
            print('Ignoring %s' % f)
            continue

        try:
            size = os.stat(f).st_size
        # Unresolvable symlink, just ignore those
        except FileNotFoundError as exc:
            print('%s is unresolvable, skipping (%s)' % (f, exc))
            continue

        if size == 0: continue
        if size > 1024 ** 2:
            print("Skipping `%s' because it's over 1MiB" % f)
            continue

        try:
            data = open(f, 'rb').read()
        except (OSError, PermissionError) as exc:
            print("Error: Unable to read `%s': %s" % (f, exc))
            continue

        if is_binary(data):
            print("Skipping `%s' because it looks binary" % f)
            continue

        data = data.split(b'\n')

        fixed_indent = False
        for i, line in enumerate(data):
            # Fix indentation
            repl_count = 0
            while line.startswith(indent_find):
                fixed_indent = True
                repl_count += 1
                line = line.replace(indent_find, b'', 1)

            if repl_count > 0:
                line = indent_replace * repl_count + line

        data = list(filter(lambda x: x is not None, data))

        try:
            open(f, 'wb').write(b'\n'.join(data))
        except (OSError, PermissionError) as exc:
            print("Error: Unable to write to `%s': %s" % (f, exc))


if __name__ == '__main__':
    allfiles = []
    for root, dirs, files in os.walk(os.getcwd()):
        for f in files:
            p = '%s/%s' % (root, f)
            if do_add:
                allfiles.append(p)

    run(allfiles)
于 2015-08-12T14:11:14.000 回答
5

我的建议是使用:

find . -name '*.lua' -exec ex '+%s/\t/  /g' -cwq {} \;

注释:

  1. 使用就地编辑。将备份保存在 VCS 中。无需生成 *.orig 文件。在任何情况下,最好将结果与您上次提交的结果进行比较,以确保它按预期工作。
  2. sed是一个流编辑器。用于ex就地编辑。这避免了为每个替换创建额外的临时文件和生成 shell,如最佳答案中所示。
  3. 警告:这会弄乱所有选项卡,而不仅仅是那些用于缩进的选项卡。它也不会对标签进行上下文感知替换。这对我的用例来说已经足够了。但你可能无法接受。
  4. 编辑:使用此答案的早期版本find|xargs而不是find -exec. 正如@gniourf-gniourf 所指出的,这会导致文件名中的空格、引号和控制字符出现问题。惠勒
于 2016-06-14T11:26:28.070 回答
4

在找到混合制表符和空格后,我曾经astyle重新缩进所有 C/C++ 代码。如果您愿意,它还具有强制特定支撑样式的选项。

于 2013-09-29T15:54:56.317 回答
4

可以使用vim

find -type f \( -name '*.css' -o -name '*.html' -o -name '*.js' -o -name '*.php' \) -execdir vim -c retab -c wq {} \;

正如 Carpetsmoker 所说,它会根据您的设置重新vim设置标签。和文件中的模式,如果有的话。此外,它不仅会替换行首的制表符。这不是您通常想要的。例如,您可能有包含制表符的文字。

于 2014-11-10T08:28:29.177 回答
4

要递归地将目录中的所有 Java 文件转换为使用 4 个空格而不是制表符:

find . -type f -name *.java -exec bash -c 'expand -t 4 {} > /tmp/stuff;mv /tmp/stuff {}' \;
于 2016-06-29T16:05:27.223 回答
3

没有提到身体rpl?使用 rpl 您可以替换任何字符串。要将制表符转换为空格,

rpl -R -e "\t" "    "  .

很简单。

于 2019-03-22T01:08:15.990 回答
3

下载并运行以下脚本,以递归方式将纯文本文件中的硬标签转换为软标签。

从包含纯文本文件的文件夹中执行脚本。

#!/bin/bash

find . -type f -and -not -path './.git/*' -exec grep -Iq . {} \; -and -print | while read -r file; do {
    echo "Converting... "$file"";
    data=$(expand --initial -t 4 "$file");
    rm "$file";
    echo "$data" > "$file";
}; done;
于 2017-01-31T14:12:40.210 回答
3

Git存储库友好方法

git-tab-to-space() (
  d="$(mktemp -d)"
  git grep --cached -Il '' | grep -E "${1:-.}" | \
    xargs -I'{}' bash -c '\
    f="${1}/f" \
    && expand -t 4 "$0" > "$f" && \
    chmod --reference="$0" "$f" && \
    mv "$f" "$0"' \
    '{}' "$d" \
  ;
  rmdir "$d"
)

作用于当前目录下的所有文件:

git-tab-to-space

仅作用于 C 或 C++ 文件:

git-tab-to-space '\.(c|h)(|pp)$'

你可能特别想要这个,因为那些烦人的 Makefiles 需要标签。

命令git grep --cached -Il ''

  • 只列出跟踪的文件,所以里面没有.git
  • 排除目录、二进制文件(将被损坏)和符号链接(将被转换为常规文件)

如解释:如何列出 git 存储库中的所有文本(非二进制)文件?

chmod --reference保持文件权限不变:https ://unix.stackexchange.com/questions/20645/clone-ownership-and-permissions-from-another-file不幸的是我找不到简洁的 POSIX 替代方案

如果您的代码库有允许在字符串中使用功能性原始选项卡的疯狂想法,请使用:

expand -i

然后玩得开心一一浏览所有非行首选项卡,您可以列出这些选项卡:Is it possible to git grep for tabs?

在 Ubuntu 18.04 上测试。

于 2018-09-02T11:21:53.250 回答
2

The use of expand as suggested in other answers seems the most logical approach for this task alone.

That said, it can also be done with Bash and Awk in case you may want to do some other modifications along with it.

If using Bash 4.0 or greater, the shopt builtin globstar can be used to search recursively with **.

With GNU Awk version 4.1 or greater, sed like "inplace" file modifications can be made:

shopt -s globstar
gawk -i inplace '{gsub("\t","    ")}1' **/*.ext

In case you want to set the number of spaces per tab:

gawk -i inplace -v n=4 'BEGIN{for(i=1;i<=n;i++) c=c" "}{gsub("\t",c)}1' **/*.ext
于 2015-03-31T01:50:26.597 回答
-1

使用 vim 方式:

$ ex +'bufdo retab' -cxa **/*.*
  • 做好备份!在执行上述命令之前,因为它可能会损坏您的二进制文件。
  • 要使用globstar( **) 进行递归,请通过 激活shopt -s globstar
  • 要指定特定的文件类型,请使用例如:**/*.c.

要修改制表位,请添加+'set ts=2'.

但是缺点是它可以替换字符串中的制表符

因此,对于稍微更好的解决方案(通过使用替换),请尝试:

$ ex -s +'bufdo %s/^\t\+/  /ge' -cxa **/*.*

或者通过使用ex编辑器+expand实用程序:

$ ex -s +'bufdo!%!expand -t2' -cxa **/*.*

对于尾随空格,请参阅:如何删除多个文件的尾随空格?


您可以将以下功能添加到您的.bash_profile

# Convert tabs to spaces.
# Usage: retab *.*
# See: https://stackoverflow.com/q/11094383/55075
retab() {
  ex +'set ts=2' +'bufdo retab' -cxa $*
}
于 2015-04-19T22:58:48.680 回答
-1

仅在“.lua”文件中将制表符转换为空格 [制表符 -> 2 个空格]

find . -iname "*.lua" -exec sed -i "s#\t#  #g" '{}' \;
于 2013-10-26T23:52:48.740 回答