linux - 在 Bash 中用嵌套括号替换文本的第一个实例的最佳方法?

标签 linux bash shell sed

我对 Linux/Bash 比较陌生,我正在将以下指南作为我自己的教程:

https://www.digitalocean.com/community/tutorials/how-to-install-elasticsearch-logstash-and-kibana-elk-stack-on-centos-7

尽管如此,我还尝试编写一个 bash 脚本来执行该指南中的所有步骤,而不仅仅是手动执行此操作。

我陷入了以下步骤:指南要求您安装 nginx,然后从 nginx.conf 文件中删除以下文本 block :

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }enter code here

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }

我试图使用 sed 来完成此操作,使用以下命令:

sed -i '/server {/,/ location = /50x.html {            }        }/d' /etc/nginx/nginx.conf

但我无法让它成功匹配 block 的末尾(我无法弄清楚的正则表达式/特殊字符/空格错误,例如“sed:-e表达式#1,字符10:未终止”地址正则表达式”)。我尝试转义特殊字符,但仍然卡住。

我放弃了这一点,而是选择删除 block 开头匹配的固定数量的行:

sudo sed -i '/server {/,+19d' /etc/nginx/nginx.conf

这有效,但也删除了文件中其他地方我不想要的注释文本中以“/server {/”开头的另一个 block 。我进行了搜索,但无法找到仅在使用此命令第一次匹配后删除的方法。我看到了以下建议:

sudo sed -i '0,/server {/,+19d' /etc/nginx/nginx.conf

但这些返回错误:sed: -e 表达式 #1, char 13: 未知命令: `,'

总而言之,我的问题是:

如何使用 sed 删除嵌套括号 block ?如果不可能,那么有什么更好的工具可以使用?如何仅在第一个匹配的出现时进行删除?

非常感谢您的阅读和帮助。

最佳答案

任何时候你遇到涉及嵌套结构(括号、大括号等)的问题时,你可以立即知道这个问题对于sed来说太难了,或者实际上任何仅适用于正则表达式的工具。这是计算理论的结果,对此有一个不错的背景解释Wikipedia page :基本上,正则表达式仅实现有限状态自动机,但您需要具有下推自动机计算能力的东西。

另一种说法是“正则表达式无法计数”:在通用正则表达式执行器中,任意数量的左括号“(((”(之间可能有任意数量的非括号字符)结束您可以编写(极其复杂的)正则表达式,该正则表达式可以以不同于双括号组的方式处理三括号组,并且这两者都不同于单括号组,但随后有人出现并编写“( ((("对你来说,破坏了你复杂的方案。因此,你写了一个指数级复杂的方法来处理带有四个括号的组,然后有人给了你五个......:-)

无论如何,结果是您需要一种更强大的语言。这些确实存在,并且您可以在 bash 脚本中编写自己的脚本(因为 bash 实现算术),但这些没有标准的现成答案。大多数人用他们认为方便的任何语言编写小型解析器,或者您可以使用 Python 和 ply 包,或者使用 C 或 C++ 的 bison 或 yacc(包含在 Linux 中),尽管这些实际上是成熟的工具用于编写编译器的解析部分。

您还可以在 awk 中编写完整的解析器,使用 awk 的正则表达式来实现分词器。我已经为玩具示例完成了此操作,但不推荐这样做:一旦您学习如何使用 lex 和 yacc(或 Python 中的 ply),您可能会发现使用它们实际上更容易。由于它们功能非常齐全,您可以用它们编写真正的工具。

我建议在这里使用 ply,因为 Python 以易于使用的方式提供了所有复杂的存储管理位。请注意,词法分析和解析是一个相当大的主题,而编译器则是一个更大的主题。标记化和解析的概念并不难,只是有一系列令人难以置信的数学支撑着不同的方法,以及什么上下文无关语法意思和暗示(点击维基百科链接)。


编辑:这是仅使用层的扫描仪部分的完整实现。它有点长,但它展示了如何使用 ply 构建词法分析器,然后以一种相当俗气的方式使用它。

我不知道我对字符串和其他标记的处理是否正确,因为有关 nginx 输入文件格式的文档相当薄,但由于标记是由正则表达式定义的,因此如果需要,应该很容易调整。我也不认为它是实现 nginx 输入文件解析器的特别好的方法:如果你真的想读取并解释文件,而不是简单地破解它,你会可能想要一些至少有点不同的东西,也许包括正确的语法。

#! /usr/bin/env python

from __future__ import print_function

import argparse
import collections
import sys

import ply.lex

t_COMMENT = r'\#.*'
t_BACKSLASHED = r'\\([\\{}])'
t_WORD = '[A-Za-z0-9_]+'
t_STRING = '("[^"]*")|' "('[^']*)'"
t_LB = '{'
t_RB = '}'
t_WHITE = '[ \t]+'
t_REST = '.'

tokens = [
   'COMMENT',
   'BACKSLASHED',
   'WORD',
   'STRING',
   'LB',
   'RB',
   'WHITE',
   'REST',
   'NEWLINE',
]

def t_NEWLINE(t):
    r'\n+'
    t.lexer.lineno += len(t.value)
    return t

# This never happens because '.' matches anything but newline and
# we have a newline rule; but if we don't define it, ply complains.
def t_error(t):
    print("Illegal character '%s'" % t.value[0])
    t.lexer.skip(1)

# Build the lexer
LEXER = ply.lex.lex()

def fill(tlist, howmany):
    "build up token list - returns False if the list is all non-tokens"
    while len(tlist) < howmany:
        tlist.append(LEXER.token())
    return tlist[0] is not None

def nth_is(tlist, offset, tok_type, tok_value=None):
    "a sleazy kind of parser lookahead"
    fill(tlist, offset + 1)
    tok = tlist[offset]
    if tok is None:
        return False
    if tok.type != tok_type:
        return False
    if tok_value is not None and tok.value != tok_value:
        return False
    return True

TEST_DATA = '''\
# a comment - gets copied
server {
    stuff;
    more { } stuff;
    this is not a brace \{ because it is backslashed;
    "and these strings }";
    'do not close the server } either';
}
this gets copied;
'''

def main():
    "main"

    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--test', action='store_true')
    parser.add_argument('inputfile', nargs='?', type=argparse.FileType('r'),
                        default=sys.stdin)

    args = parser.parse_args()
    if args.test:
        LEXER.input(TEST_DATA)
    else:
        LEXER.input(args.inputfile.read())

    # Tokenize; copy lines through except when dealing
    # with the first "server" definition
    looking_for_server = True
    copying = True
    eat_white_space_and_newline = False
    brace_depth = 0
    tlist = collections.deque()
    while fill(tlist, 1):
        if tlist[0].type == 'LB':
            brace_depth += 1
        elif tlist[0].type == 'RB':
            if brace_depth > 0:
                brace_depth -= 1
                # If we went from 1 to 0 and are in
                # non-copy mode, resume copying, but eat
                # one white-space-and-newline
                if brace_depth == 0 and not copying:
                    copying = True
                    eat_white_space_and_newline = True
                    tlist.popleft() # eat the }
                    continue
        if looking_for_server:
            check = 0
            if tlist[0].type == 'WHITE':
                fill(tlist, 2)
                check = 1
            else:
                check = 0
            if nth_is(tlist, check, 'WORD', 'server'):
                # server followed by spaces and {, or by { => stop copying
                if nth_is(tlist, check + 1, 'LB') or (
                        nth_is(tlist, check + 1, 'WHITE') and
                        nth_is(tlist, check + 2, 'LB')):
                    copying = False
                    looking_for_server = False
                if check > 0:
                    tlist.popleft() # toss white space at 0 now
            # We'll increment brace-depth when we actually consume
            # the brace.
        if copying:
            if not eat_white_space_and_newline or \
                    tlist[0].type not in ('NEWLINE', 'WHITE'):
                print(tlist[0].value, end='')
            if tlist[0].type == 'NEWLINE':
                eat_white_space_and_newline = False
        tlist.popleft()

if __name__ == '__main__':
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit('\nInterrupted')

关于linux - 在 Bash 中用嵌套括号替换文本的第一个实例的最佳方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48028973/

相关文章:

C++ Linux获取MAC地址在同一台计算机上返回不同的值

linux - 解释 UNIX 中不同的 EXIT 命令

bash - 查找已使用 bzr 提交的字符串变量名称的更改

linux - 重命名以数值结尾的文本文件

bash - 获取定时进程的 PID 以及时间的输出

linux - 通过删除字母从 shell 重命名文件

linux - 在 linux 服务器上托管 asp 页面

python - python shell 可以有一些预输入吗?

linux - 运行 chroot 命令时出现错误

bash - 无法在 Terraform 中运行 shell 脚本