关于bash:如何将标签转换为目录的每个文件中的空格?

How can I convert tabs to spaces in every file of a directory?

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

另外,有没有办法设置每个标签的空格数?


使用sed进行简单替换是可以的,但不是最好的解决方案。如果标签之间存在"额外"空格,则替换后它们仍然存在,因此边距将是不规则的。在行中间展开的选项卡也无法正常工作。在bash中,我们可以说

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

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

You should always make a backup copy of the tree before trying something like this in case something goes wrong.


尝试使用命令行工具expand

1
expand -i -t 4 input | sponge output

哪里

  • -i用于仅展开每行的前导标签;
  • -t 4表示每个选项卡将转换为4个空格字符(默认为8)。
  • sponge来自moreutils包,并避免清除输入文件。

最后,在使用Homebrew(brew install coreutils)安装coreutils之后,可以在OSX上使用gexpand


Warning: This will break your repo.

This will corrupt binary files, including those under svn, .git! Read the comments before using!

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

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

缺点:

  • 将替换文件中的标签。
  • 如果您碰巧在此目录中有5GB的SQL转储,则需要很长时间。


从Gene的答案中收集最好的评论,到目前为止,最好的解决方案是使用来自moreutils的sponge

1
2
3
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文件。


使用反斜杠转义sed

在linux上:

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

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

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

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

在mac上:

  • 在所有* .txt文件中替换所有4个空格的选项卡:

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


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

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

如果您在信任它之后想要它是静默的,那么只需在最后的sh命令上删除-v

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

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

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

1
find mod/ -name '*.php' -mindepth 1 -maxdepth 2 | awk '{print"expand -t 4", $0,"> /tmp/e; mv /tmp/e", $0}' | sh


How can I convert tabs to spaces in every file of a directory (possibly
recursively)?

这通常不是你想要的。

你想为png图像做这个吗? PDF文件? .git目录?您的
Makefile(需要标签)?一个5GB的SQL转储?

理论上,你可以将大量的exlude选项传递给find或其他任何东西
否则你正在使用;但这很脆弱,一旦你添加其他东西就会中断
二进制文件。

你想要的,至少是:

  • 跳过特定大小的文件。
  • 通过检查是否存在NULL字节来检测文件是否为二进制文件。
  • 仅替换文件开头的选项卡(expand执行此操作,sed
    没有)。
  • 据我所知,没有"标准"的Unix实用程序可以做到这一点,并且使用shell一行代码并不是很容易,因此需要一个脚本。

    前段时间我创建了一个名为的小脚本
    完全正确的sanitize_files
    那。它还修复了一些其他常见的东西,例如用
    替换


    添加尾随

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

    我还想指出,在回答其他一些答案时,
    使用shell globbing并不是一种强有力的方法,因为更快
    或者以后你会得到比ARG_MAX更多的文件(现代版本)
    Linux系统是128k,这可能看起来很多,但迟早不是
    足够)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    #!/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'
    ')

            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'
    '.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)


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

    1
    pr -t -e=4 file > file.expanded
    • -t会抑制标题
    • -e=num将标签扩展为num个空格

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

    1
    2
    3
    4
    5
    6
    7
    8
    #!/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

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

    注意:

  • 在git或svn repo中这样做可能很危险
  • 如果您的代码文件中包含嵌入字符串文字的选项卡,则这不是正确的解决方案

  • 要在目录中递归转换所有Java文件以使用4个空格而不是选项卡:

    1
    find . -type f -name *.java -exec bash -c 'expand -t 4 {} > /tmp/stuff;mv /tmp/stuff {}' \;


    我的建议是使用:

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

    评论:

  • 使用就地编辑。将备份保留在VCS中。无需生成* .orig文件。在任何情况下,最好将结果与最后一次提交区分开来,以确保它按预期工作。
  • sed是一个流编辑器。使用ex进行就地编辑。这样可以避免为每个替换创建额外的临时文件和产生shell,如上面的答案所示。
  • 警告:这会使所有标签混乱,而不仅仅是用于压痕的标签。此外,它不会对标签进行上下文感知替换。这对我的用例来说足够了。但可能不适合你。
  • 编辑:此答案的早期版本使用find|xargs而不是find -exec。正如@ gniourf-gniourf所指出的,这会导致文件名中的空格,引号和控制字符出现问题。惠勒。

  • 您可以将findtabs-to-spaces包一起使用。

    首先,安装tabs-to-spaces

    1
    npm install -g tabs-to-spaces

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

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

    这将在每个文件中用2 spaces替换每个tab字符。


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    #!/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;

    在找到混合标签和空格后,我使用astyle重新缩进所有的C / C ++代码。如果您愿意,它还可以选择强制特定的支撑样式。


    没有人提到rpl?使用rpl,您可以替换任何字符串。
    要将标签转换为空格,

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

    非常简单。


    在其他答案中建议使用expand似乎是单独完成此任务的最合理的方法。

    也就是说,它也可以用Bash和Awk完成,以防你可能想要做一些其他修改。

    如果使用Bash 4.0或更高版本,内置globstar的shopt可用于以**递归搜索。

    使用GNU Awk 4.1或更高版本,可以进行"inplace"文件修改:

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

    如果您想设置每个标签的空格数:

    1
    gawk -i inplace -v n=4 'BEGIN{for(i=1;i<=n;i++) c=c""}{gsub("\t",c)}1' **/*.ext

    可以使用vim

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

    正如Carpetsmoker所说,它将根据您的vim设置进行重新标记。和文件中的模型,如果有的话。此外,它不仅会在行的开头替换标签。这不是你通常想要的。例如,您可能有文字,包含标签。


    Git存储库友好的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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"
    )

    对当前目录下的所有文件执行操作:

    1
    git-tab-to-space

    仅对C或C ++文件起作用:

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

    你可能想要这个特别是因为那些需要制表符的恼人的Makefile。

    命令git grep --cached -Il ''

    • 仅列出跟踪的文件,因此.git内没有任何内容
    • 排除目录,二进制文件(会被破坏)和符号链接(将被转换为常规文件)

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

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

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

    1
    expand -i

    然后一个接一个地浏览所有非开始行标签的乐趣,您可以列出:是否可以为标签git grep?

    在Ubuntu 18.04上测试过。


    将标签转换为".lua"文件中的空格[tabs - > 2个空格]

    1
    find . -iname"*.lua" -exec sed -i"s#\t#  #g" '{}' \;


    使用vim-way:

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

    要修改tabstop,请添加+'set ts=2'

    然而,缺点是它可以替换字符串内的标签。

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

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

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

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

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

    您可以在.bash_profile中添加以下功能:

    1
    2
    3
    4
    5
    6
    # Convert tabs to spaces.
    # Usage: retab *.*
    # See: https://stackoverflow.com/q/11094383/55075
    retab() {
      ex +'set ts=2' +'bufdo retab' -cxa $*
    }