根据 awk 的缩进连接不同行上的字符串

根据 awk 的缩进连接不同行上的字符串

我有这个多行字符串:

foo
foobar
  bar
  baz
    bat
bar

我想最终得到:

foo
foobar.bar
foobar.baz
foobar.baz.bat
bar

我的想法是,对于每一行,我必须查看以下所有行以检查下一个n字符串是否以一定数量的空格开头,并且根据空格数我必须格式化字符串因此。

这可以用 awk 实现吗?

答案1

假设GNUgawk...但不包括防错功能(相信你的话:“这是文字文件”) ... 所以,:

$ cat file
foo
foobar
  bar
  baz
    bat
bar
$
$ gawk 'BEGIN {
    PROCINFO["sorted_in"] = "@ind_num_asc"
}

{
    match($0, /^[ \t]*/)
    if (RLENGTH == 0) {
        if (NR > 1 && length(a) == 1) {
            print a[0]
        }
        delete a
        a[0] = $0
        lsnum = RLENGTH
    }
    if (RLENGTH > lsnum) {
        lsnum = RLENGTH
        a[lsnum] = "." substr($0, RLENGTH + 1)
        p = 1
    }
    if (p == 1) {
        for (i in a) {
            printf "%s", a[i]
        }
        print ""
        lsnum = 0
        p = 0
    }
}

END {
    if (length(a) == 1) {
        print a[0]
    }
}' file
foo
foobar.bar
foobar.baz
foobar.baz.bat
bar

如果文件中的行前面带有空格或制表符,那么这应该有效...但是,对于两者的混合,您可能需要进行一些调整,例如将制表符解析为空格,反之亦然,以避免重复索引,这将设置错误的数组元素并导致错误的输出。

答案2

perl

<your-file expand | perl -lpe '
  ($indent, $txt) = /^( *)(.*)/;
  $depth = length($indent) / 2;
  $part[$depth] = $txt;
  $_ = join ".", @part[0..$depth]'

或者打高尔夫球:

<your-file expand|perl -lpe'
  my$d;/^(  (?{$d++}))*/;$p[$d]=$'\'';$_=join".",@p[0..$d]'

(如果行开头有奇数个空格,也允许文本以一个空格字符开头)。

expand将制表符(如果有)扩展为空格,假设制表符每 8 列停止一次,但这可以通过选项进行更改。

等价awk的可能是:

<your-file expand | awk '
  BEGIN {
    OFS = "."
    while ((getline line) > 0) {
      match(line, /^ */)
      $ (NF = RLENGTH / 2 + 1) = substr(line, RLENGTH + 1)
      print
    }
  }'

请注意,他们给出:

foo
foobar
foobar.bar
foobar.baz
foobar.baz.bat
bar

我假设foobar您的预期输出中缺少的行是一个疏忽。

答案3

让我们假设数据实际上是一个 YAML 文档,只包含null各个路径上的值。我们可以通过:在问题数据的行尾添加来做到这一点:

$ sed 's/$/:/' file
foo:
foobar:
  bar:
  baz:
    bat:
bar:

这允许我们向 YAML 解析器询问文档中的所有可用路径。


安德烈·基斯柳克yq:

$ sed 's/$/:/' file | yq -r 'paths | join(".")'
foo
foobar
foobar.bar
foobar.baz
foobar.baz.bat
bar

paths的过滤器(jqyq是一个包装器)输出一组表示给定文档中所有路径的数组。该join()调用将这些列表连接成点分隔的字符串。

可以通过仅选择具有值的路径来避免列出中间路径nullnot这是一种较短的书写方式. == null):

$ sed 's/$/:/' file | yq -r 'paths(not) | join(".")'
foo
foobar.bar
foobar.baz.bat
bar

请注意,这会删除中间路径foobarfoobar.baz


迈克·法拉赫yq:

$ sed 's/$/:/' file | yq '.. | map(path | join(".")) | .[]'
foo
foobar
bar
foobar.bar
foobar.baz
foobar.baz.bat

我们需要使用 Mike's 显式地递归文档的值yq,并安排在获取值时调用path和。join()

您也可以使用较短的表达式来完成此操作..|path|join("."),但随后您会在输出中得到一个额外的空行。

您是否想避免输出中间路径:

$ sed 's/$/:/' file | yq '.. | map(select(not) | path | join(".")) | .[]'
foo
bar
foobar.bar
foobar.baz.bat

答案4

这是一个解决方案TXR 口齿不清。该方法是从项目中构建一个嵌套树,然后可以轻松地以点符号打印(或以有趣的方式进行分析)。这可以处理突然缩进过多级别的项目,如处理扩展输入文件中所示input2,以及意外取消缩进的情况。

$ cat input2
foo
foobar
  bar
  baz
    bat
bar
xyzzy
      out1
      out2
stretched
      way
          out
weird
      indent
    deindent
$ txr code.tl < input2
foo
foobar
foobar.bar
foobar.baz
foobar.baz.bat
bar
xyzzy
xyzzy.out1
xyzzy.out2
stretched
stretched.way
stretched.way.out
weird
weird.indent
weird.deindent

到代码中code.tl

(defsymacro indent "  ")

(defun deindent (items)
  (mapcar (do if (starts-with indent @1)
            (drop (len indent) @1)
            @1)
          items))

(defun indent-to-tree (lines)
  (let ((pieces (partition-if (opip (starts-with indent @2) not) lines)))
    (append-matches (@(as piece (@head . @tail)) pieces)
      (cond
        ((starts-with indent head)
         (while* (starts-with indent (car piece))
           (upd piece deindent))
         (indent-to-tree piece))
        (t (list (cons head (indent-to-tree (deindent tail)))))))))

(defun dot-notation (items)
  (build
    (each ((item items))
      (tree-bind (node . children) item
        (add node)
        (each ((dnc (dot-notation children)))
          (add `@node.@dnc`))))))

(flow (get-lines) indent-to-tree dot-notation tprint)

在顶层,工作流程由flow宏组织:标准输入被转换为字符串列表,(get-lines)然后通过管道传递indent-to-tree以获得树结构,然后将其转换dot-notation为字符串列表,然后转储为标准输出上的行通过tprint.

树的转换是一个递归过程。递归的每个级别都从将行列表分成组开始,这样每条未缩进的行都开始一个新组。

我们可以在 REPL 中看到它的样子:

1> (flow (file-get-lines "input2")
         (partition-if (opip (starts-with indent @2) not)))
(("foo") ("foobar" "  bar" "  baz" "    bat") ("bar") ("xyzzy" "      out1" "      out2")
 ("stretched" "      way" "        out") ("weird" "      indent" "    deindent"))

indent-to-tree函数执行此分区,然后迭代各个部分,处理两种主要情况:仅缩进组或以非缩进行开头的缩进组。这些案例是递归处理的。在仅缩进组中,我们迭代地取消缩进,直到第一行不再缩进,然后对其进行递归。

当我们应用indent-to-tree到 中的行时input2,它看起来像这样:

2> (flow (file-get-lines "input2") indent-to-tree)
(("foo") ("foobar" ("bar") ("baz" ("bat"))) ("bar") ("xyzzy" ("out1") ("out2"))
 ("stretched" ("way" ("out"))) ("weird" ("indent") ("deindent")))

这是树表示:树的顶层是项目列表。每个项目都是一个头字符串(在代码中称为node),后跟零个或多个子项目。例如,有没有子项的("foo")头字符串。"foo"而有孩子的("foobar" ....)头和。其中第二个有一个孩子,而它本身没有孩子。"foobar"("bar")("baz" ("bat"))("bat")

这种表示法通过一个简单的递归过程变成了点表示法,这几乎只是整个过程中的一个脚注。dot-notation依赖于build为程序构建列表创建词汇环境的宏。在内部build,我们可以使用(add ...)将项目添加到列表中。add出现两次:添加头字符串,以便foobar列出没有子项的字符串,然后还为每个子项再次列出它,并附加该子项的点符号。

笔记:

  • indent如果我们定义为(一个空格),一切仍然有效" ",所以我们应该这样做,因为这可以处理更多情况,例如缩进未与两个空格的倍数对齐。

  • 挤出空白行是个好主意;这还没有完成,而且行为也不好。

  • 中的谓词函数partition-if是一个双参数函数,这就是op语法引用参数 的原因@2partition-if使用连续的、重叠的元素对调用该函数。当函数返回 true 时,它​​将在这些元素之间划分序列。例如

    ;; start new partition whenever an element is smaller
    ;; than its predecessor.
    (partition-if (op > @1 @2) '(1 2 3 4 3 2 1 0 1 2 3))
    --> ((1 2 3 4) (3) (2) (1) (0 1 2 3))
    
  • while*while是进行循环底部测试的变体。另一种看待它的方式是,它在第​​一次迭代之前跳过循环保护,无条件执行主体,然后表现得像while.

相关内容