【问题标题】:How can I convert tabs to spaces in every file of a directory?如何在目录的每个文件中将制表符转换为空格?
【发布时间】:2012-06-21 02:29:21
【问题描述】:

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

另外,有没有办法设置每个制表符的空格数?

【问题讨论】:

  • 您想替换文件或文件名中的制表符吗?
  • pr 是一个很棒的实用程序。看到这个answer
  • 不建议用空格替换制表符,因为它会伤害使用相同文件的其他人。只需将工具调整为所需的标签宽度即可。

标签: bash shell unix spaces in-place


【解决方案1】:

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

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

跳过二进制文件的逻辑来自this post

注意:

  1. 在 git 或 svn 存储库中这样做可能很危险
  2. 如果您的代码文件在字符串文字中嵌入了裸标签,这不是正确的解决方案

【讨论】:

【解决方案2】:

试试命令行工具expand

expand -i -t 4 input | sponge output

在哪里

  • -i 用于仅展开每行的前导标签;
  • -t 4 表示每个选项卡将被转换为 4 个空白字符(默认为 8 个)。
  • sponge 来自moreutils 包,并避免clearing the input file。在 macOS 上,moreutils 包可通过Homebrew (brew install moreutils) 或MacPorts (sudo port install moreutils) 获得。

最后,在安装coreutilsHomebrew (brew install coreutils) 或MacPorts (sudo port install coreutils) 之后,您可以在macOS 上使用gexpand

【讨论】:

  • 这是GNU_Core_Utilities之一
  • 您应该将-i 传递给expand 以仅替换每行的前导制表符。这有助于避免替换可能是代码一部分的选项卡。
  • 如何递归处理目录中的每个文件?
  • 每次我尝试使用它都会使一些(通常是所有)文件空白。 :\
  • @ThorSummoner: 如果inputoutput 是同一个文件,bash 甚至会在启动expand 之前破坏内容。这就是> 的工作原理。
【解决方案3】:

警告:这会破坏你的 repo。

会损坏二进制文件,包括svn.git 下的文件!使用前请阅读cmets!

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

原文件保存为[filename].orig

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

缺点:

  • 将替换文件中所有位置的选项卡。
  • 如果您在此目录中碰巧有 5GB 的 SQL 转储,将需要很长时间。

【讨论】:

  • 对于混合了制表符和空格的视觉空间,这种方法会导致不正确的扩展。
  • 我还将添加一个文件匹配器,例如仅针对 .php 文件 find ./ -iname "*.php" -type f -exec sed -i 's/\t/ /g' {} \;
  • 不要使用 SED!如果字符串中有嵌入的选项卡,您最终可能会破坏您的代码。这就是expand 命令要处理的内容。使用expand
  • @DavidW。我只需更新此命令以仅替换行首的制表符。 find ./ -type f -exec sed -i 's/^\t/####/g' {} \;。但我不知道扩展命令 - 非常有用!
  • 不要使用!这个答案也破坏了我的本地 git 存储库。如果您有包含混合制表符和空格的文件,它将插入 # 序列。请改用 Gene 的答案或下面 Doge 的评论。
【解决方案4】:

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

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

很简单。

【讨论】:

  • 这损坏了我的仓库中的所有二进制文件。
  • 一个很好的命令,但是对于递归和上面指定的文件夹中的所有文件选项有潜在的危险。我会添加 --dry-run 选项“以防万一”以确保您位于正确的文件夹中。
【解决方案5】:

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 内没有任何内容
  • 排除目录、二进制文件(将被损坏)和符号链接(将被转换为常规文件)

解释于:How to list all text (non-binary) files in a git repository?

chmod --reference 保持文件权限不变:https://unix.stackexchange.com/questions/20645/clone-ownership-and-permissions-from-another-file 不幸的是我can't find a succinct POSIX alternative

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

expand -i

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

在 Ubuntu 18.04 上测试。

【讨论】:

    【解决方案6】:

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

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

    #!/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;
    

    【讨论】:

      【解决方案7】:

      Gene's answer 收集最好的 cmets,目前最好的解决方案是使用来自moreutilssponge

      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") 在这里不起作用,因为it would overwrite the file too soon

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

      【讨论】:

      • TIL:神奇的海绵命令,在使用 Linux 15 年后。感谢来自网络的神秘骑士。
      【解决方案8】:

      您可以为此使用 findtabs-to-spaces 包。

      首先,安装tabs-to-spaces

      npm install -g tabs-to-spaces
      

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

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

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

      【讨论】:

        【解决方案9】:

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

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

        【讨论】:

        • 这个答案与 4 年前发布的 this 有何不同?
        • 您的回答也是如此。事实上,这是 Gene 答案的劣质版本:1)Gene 的答案会处理同名的目录。 2) 如果展开失败,它不会移动
        【解决方案10】:

        我的建议是使用:

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

        评论:

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

        【讨论】:

        • ex 可能并非在每个 Unix 系统上都可用。用vi -e 代替它可能适用于更多机器。此外,您的正则表达式用两个空格替换任意数量的起始制表符。将正则表达式替换为 +%s/\t/ /g 以不破坏多级缩进。但是,这也会影响不用于缩进的制表符。
        • ex 是 POSIX [1] 的一部分,因此应该可用。关于多级缩进的好点。我实际上在我的文件中使用了/\t/ / 变体,但选择了/\t\+// 来不破坏非缩进制表符。错过了多缩进的问题!更新答案。 [1]man7.org/linux/man-pages/man1/ex.1p.html#SEE%C2%A0ALSO
        • 以这种方式使用xargs 是无用的、低效的和损坏的(想想包含空格或引号的文件名)。为什么不使用find-exec 开关呢?
        • 我认为带有空格和引号的文件名已损坏; ) 如果您需要支持,我会选择:-print0 查找/xargs 的选项。我喜欢 xargs 而不是 -exec,因为:a)关注点分离 b)它可以更容易地与 GNU 并行交换。
        • 更新添加@gniourf_gniourf cmets。
        【解决方案11】:

        我喜欢上面递归应用程序的“查找”示例。为了使其成为非递归的,仅更改当前目录中匹配通配符的文件,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
        

        【讨论】:

        • Shell globbing 迟早会中断,因为文件名的总数只能是ARG_MAX 长度。在 Linux 系统上这是 128k,但我已经多次遇到此限制,以至于不依赖 shell globbing。
        • 你真的不需要适应它们。 find 可以告诉-maxdepth 1,它只处理被修改目录的条目,而不是整个树。
        【解决方案12】:

        使用反斜杠转义的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
          

        【讨论】:

        • @Маша sed -i '' $'s/\t/ /g' $(find . -name "*.txt")
        • 这个答案似乎是最简单的。
        【解决方案13】:

        在其他答案中建议使用expand 似乎是仅此任务最合乎逻辑的方法。

        也就是说,它也可以使用 Bash 和 Awk 来完成,以防您可能想要对其进行一些其他修改。

        如果使用 Bash 4.0 或更高版本,shopt builtin globstar 可用于递归搜索 **

        使用 GNU Awk 4.1 或更高版本,可以进行类似 sed 的“就地”文件修改:

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

        如果您想设置每个制表符的空格数:

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

        【讨论】:

          【解决方案14】:

          为此可以使用vim

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

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

          【讨论】:

          • :retab 将更改文件中的所有选项卡,而不是开头的选项卡。它还取决于您的 :tabstop:expandtab 在 vi​​mrc 或模式行中的设置,所以这可能根本不起作用。
          • @Carpetsmoker 关于行首标签的好点。这里的任何解决方案都可以处理这种情况吗?至于tabstopexpandtab 设置,如果你使用vim 就可以了。除非文件中有模式行。
          • @x-yuri 好问题,但通常没有实际意义。大多数人在文字中使用 \t 而不是实际的制表符。
          【解决方案15】:

          使用vim方式:

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

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

          但缺点是它可以replace tabs inside the strings

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

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

          或者使用ex编辑器+expand实用工具:

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

          有关尾随空格,请参阅:How to remove trailing whitespaces for multiple files?


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

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

          【讨论】:

          • 我在这个线程中对许多答案投了反对票,而不仅仅是你的 ;-) 原因是::retab may not work at allshell globbing is a bad solution for this sort of thing,你的 :s 命令将替换 any 数量的带有 2 个空格的选项卡(您几乎从不想要),从 ex 开始只是为了运行 :!expand 进程是愚蠢的......
          • ...您的所有解决方案都会破坏二进制文件等(如 .png 文件、.pdf 文件等)
          • 坦率地说,这对于文档来说是一个可怕的建议——必须非常熟悉几个程序的一些相当不透明的语法和语义问题才能理解这一点。
          【解决方案16】:

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

          这通常不是你想要的。

          您想对 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)
          

          【讨论】:

          【解决方案17】:

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

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

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

          在尝试这样的操作之前,您应该始终对树进行备份,以防出现问题。

          【讨论】:

          • @JeffreyMartinez 好问题。 gniourf_gniourf 在 11 月 11 日编辑了我的原始答案,并对不知道使用 {} 的正确方法发表了贬低的评论。当使用-c 时,看起来他不知道$0。然后dimo414从我在转换目录中使用的临时更改为/tmp,如果/tmp在不同的挂载点上会慢得多。不幸的是,我没有可用的 Linux 机器来测试您的 $0 提案。但我认为你是对的。
          • @Gene,感谢您的澄清,这听起来像 stackoverflow 好吧 :p 。虽然我在这里,但我要补充一点,我必须在 '*.java' 周围使用引号来正确转义 *.java。
          • 如果有人从 find 中遇到“未知的主要或操作员”错误,那么这里是修复它的完整命令:find . -name '*.java' ! -type d -exec bash -c 'expand -t 4 "$0" &gt; /tmp/e &amp;&amp; mv /tmp/e "$0"' {} \;
          • 我认为这个答案没有足够的cmets,所以这是我的:如果使用joeyh.name/code/moreutils中的sponge,你可以写find . -name '*.py' ! -type d -exec bash -c 'expand -t 8 "$0" | sponge "$0"' {} \;
          • 别傻了,用find . -name '*',我刚刚销毁了我本地的git repo
          【解决方案18】:

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

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

          【讨论】:

          • 显然,选项卡展开的空间量取决于上下文。因此,sed 是一个完全不适合该任务的工具。
          • ?? @Sven,我的 sed 命令与 expand 命令的作用相同(expand -t 4 input &gt;output
          • 当然不是。 expand -t 4 会将a\tb 中的制表符扩展为3 个空格,将aa\tb 中的制表符扩展为2 个空格,就像它应该的那样。 expand 会考虑制表符的上下文,sed 不会并将制表符替换为您指定的空格数量,无论上下文如何。
          【解决方案19】:

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

          【讨论】:

            猜你喜欢
            • 2018-12-11
            • 2014-01-07
            • 1970-01-01
            • 1970-01-01
            • 2014-01-30
            • 2010-10-02
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多