我有这个多行字符串:
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 解析器询问文档中的所有可用路径。
$ sed 's/$/:/' file | yq -r 'paths | join(".")'
foo
foobar
foobar.bar
foobar.baz
foobar.baz.bat
bar
paths
的过滤器(jq
它yq
是一个包装器)输出一组表示给定文档中所有路径的数组。该join()
调用将这些列表连接成点分隔的字符串。
可以通过仅选择具有值的路径来避免列出中间路径null
(not
这是一种较短的书写方式. == null
):
$ sed 's/$/:/' file | yq -r 'paths(not) | join(".")'
foo
foobar.bar
foobar.baz.bat
bar
请注意,这会删除中间路径foobar
和foobar.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
语法引用参数 的原因@2
。partition-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
.