首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >i3配置文件解析器

i3配置文件解析器
EN

Code Review用户
提问于 2021-10-29 14:52:20
回答 2查看 239关注 0票数 4

因此,我使用i3,一个Linux管理器来管理我的窗口。此外,这是运行在笔记本电脑,经常挂载到几个输出显示器。这是由我的i3配置文件中的下列行处理的

代码语言:javascript
复制
set $firstMonitor DP-2-1
set $secondMonitor DP-1-2
set $laptop eDP-1

workspace 1 output $firstMonitor $laptop
workspace 2 output $firstMonitor $laptop
workspace 3 output $firstMonitor $laptop
workspace 4 output $firstMonitor $laptop
workspace 5 output $secondMonitor $laptop
workspace 6 output $secondMonitor $laptop
workspace 7 output $secondMonitor $laptop
workspace 8 output $secondMonitor $laptop
workspace 9 output $secondMonitor $laptop
workspace 10 output $laptop $laptop

为了方便起见,我们可以使用set在配置文件中定义变量,并以$作为前缀。上面的行告诉我的窗口管理器,如果workspace 1存在的话,我希望它能在4 on $firstMonitor上运行。否则会回到$laptop

我将此用于各种脚本,因此需要从我的配置文件中提取这些信息,并将其存储在某个地方。例如,它可能看起来像这样

代码语言:javascript
复制
[
     {
         'DP-2-1': ['1', '2', '3', '4'],
         'DP-1-2': ['5', '6', '7', '8', '9'],
         'eDP-1': ['10']
     },
     {
         'eDP-1': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
     }
]

被解析了。为此,我编写了以下Python代码,以提取分配给每个工作区的输出。注意,i3文件对空格很挑剔,这意味着行set$firstMonitor DP-2-1不会编译。

代码语言:javascript
复制
from pathlib import Path
import collections

CONFIG = Path.home() / ".config" / "i3" / "config"


def read_config(config=CONFIG):
    with open(config, "r") as f:
        return f.readlines()


def read_workspace_outputs(lines=read_config()):
    """Reads an i3 config, returns which output each workspaces is assigned to

    Example:

        set $firstMonitor DP-2-1
        set $secondMonitor DP-1-2
        set $laptop eDP-1

        set $browser 1

        workspace $browser output $firstMonitor $laptop
        workspace 2 output $firstMonitor $laptop
        workspace 3 output $firstMonitor $laptop
        workspace 4 output $firstMonitor $laptop
        workspace 5 output $secondMonitor $laptop
        workspace 6 output $secondMonitor $laptop
        workspace 7 output $secondMonitor $laptop
        workspace 8 output $secondMonitor $laptop
        workspace 9 output $secondMonitor $laptop
        workspace 10 output $laptop $laptop

        Will return

        [
            {
                'DP-2-1': ['1', '2', '3', '4'],
                'DP-1-2': ['5', '6', '7', '8', '9'],
                'eDP-1': ['10']
            },
            {
                'eDP-1': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
            }
        ]

    >>> read_workspace_outputs(lines=['workspace 1 output eDP-1'])
    [defaultdict(, {'eDP-1': ['1']})]
    >>> read_workspace_outputs(lines=['set $laptop eDP-1','workspace 1 output eDP-1'])
    [defaultdict(, {'eDP-1': ['1']})]
    >>> read_workspace_outputs(lines=['set $browser 1','workspace $browser output eDP-1'])
    [defaultdict(, {'eDP-1': ['1']})]
    >>> read_workspace_outputs(lines=[
    ...    "set $firstMonitor DP-2-1",
    ...    "set $secondMonitor DP-1-2",
    ...    "set $laptop eDP-1",
    ...    "",
    ...    "workspace 1 output $firstMonitor $laptop",
    ...    "workspace 3 output $secondMonitor $laptop",
    ...    "workspace 5 output $laptop $laptop",
    ... ])
    [defaultdict(, {'DP-2-1': ['1'], 'DP-1-2': ['3'], 'eDP-1': ['5']}), defaultdict(, {'eDP-1': ['1', '3', '5']})]
    """

    # Extract workspaces and variables [set $name command] from file
    def get_workspaces_and_variables(lines):
        workspaces = []
        variables_2_commands = dict()
        for line in lines:
            if line.startswith("workspace"):
                workspaces.append(line.strip())
            elif line.startswith("set"):
                _, variable, *command = line.split()
                variables_2_commands[variable] = " ".join(command)
        return workspaces, variables_2_commands

    # Convert back $name to command for outputs and workspaces
    def workspaces_without_variables(workspaces, variables):
        workspace_outputs = []
        for workspace in workspaces:
            workspace_str, output_str = workspace.split("output")
            workspace, variable = workspace_str.split()
            workspace_number = (
                variables[variable] if variable.startswith("$") else variable
            )
            outputs = [
                variables[output] if output.startswith("$") else output
                for output in output_str.split()
            ]
            workspace_outputs.append([workspace_number, outputs])

        return workspace_outputs

    # Currently things are stored as workspaces = [[output1, output 2], ...]
    # This flips the order and stores it as a dict with outputs as keys and values workspaces
    def workspaces_2_outputs(workspaces):
        output_workspaces = [
            collections.defaultdict(list)
            for _ in range(len(max((x[1] for x in workspaces), key=len)))
        ]
        for (workspace_number, outputs) in workspaces:
            for j, output in enumerate(outputs):
                output_workspaces[j][output].append(workspace_number)

        return output_workspaces

    workspaces_w_variables, variables = get_workspaces_and_variables(lines)
    variable_free_workspaces = workspaces_without_variables(
        workspaces_w_variables, variables
    )
    return workspaces_2_outputs(variable_free_workspaces)


if __name__ == "__main__":
    import doctest

    import yaml
    from yaml.representer import Representer

    doctest.testmod()

    OUTPUT_WORKSPACE_CONFIG = (
        Path.home() / ".config" / "i3" / "oisov-scripts" / "i3-output-workspace-2.yaml"
    )

    yaml.add_representer(collections.defaultdict, Representer.represent_dict)

    with open(OUTPUT_WORKSPACE_CONFIG, "w") as file:
        yaml.dump(read_workspace_outputs(), file)

output.yaml文件如下所示

代码语言:javascript
复制
- DP-1-2:
  - '5'
  - '6'
  - '7'
  - '8'
  - '9'
  DP-2-1:
  - '1'
  - '2'
  - '3'
  - '4'
  eDP-1:
  - '10'
- eDP-1:
  - '1'
  - '2'
  - '3'
  - '4'
  - '5'
  - '6'
  - '7'
  - '8'
  - '9'
  - '10'

将来我会添加一些输入提示,但现在我想知道这是否是提取这些信息的最佳方法。这段代码感觉有点笨重,即使是我想要的。

为了进行测试,可以使用本例中的第一个代码块。例如,较长的i3示例文件可以是https://github.com/sainathadapa/i3-wm-config/blob/master/i3-default-config-backup,请注意,这并不会将工作区设置为特定的输出。

EN

回答 2

Code Review用户

回答已采纳

发布于 2021-10-29 18:14:48

至于用Python编写代码,我认为这做得很好,包括良好的风格、注释和测试。我同意你的看法,这有点笨重,我也有一些其他的小评论。

什么是笨重的

我有一些想法,为什么它会觉得有点笨重。

get_workspaces_and_variables可以返回更多有用的对象。workspaces是未解析的原始行。variables已经很好了,它是一本可以用实际值替换符号的字典。当我试图在我的脑海中建立一个程序的心理模型时,我在这里感觉到了沉重的负担,我不得不记住两种对象,其中一种需要更多的工作。我更喜欢以直观的格式返回工作区的parse_workspaces函数。

workspaces_without_variables采用原始工作区行和变量映射,并以直观的格式返回工作区,解析所有变量。这很好,只是看起来有很多代码。最好将复制的variables[name] if name.startswith("$") else name提取到函数中。另外,我认为解析可以做得更简单一些。在代码注释中,示例原始行也会有所帮助。

最后,workspaces_2_outputs创建了最终想要的映射,我只是觉得这一行有点神秘:

for _ in范围(len(max(在工作区中为x),key=len))

也就是说,它根据长度找到最大条目,然后以长度作为范围的限制。我会发现更自然的是,采取的长度,然后找到最大的。而不是数字索引,我会使用描述性名称。如下所示:

代码语言:javascript
复制
for _ in range(max(len(outputs) for _, outputs in workspaces))]

综合上述意见,考虑这一备选实施办法:

代码语言:javascript
复制
def parse_variable(line):
    # example line: "set $firstMonitor DP-2-1"
    _, name, value = line.split()
    return name, value

def parse_workspace(line, variables):
    def resolve(name):
        return variables[name] if name.startswith('这通过了最初的测试用例,所以我认为行为是保留的,即使解析使用了一些简化的逻辑。注意,要使parse_workspaces像这样工作,解析工作区行所需的所有变量都已经定义好了,这一点很重要。我不知道i3格式,如果这是一个安全的假设或不。如果没有,那么确实需要第二次传球,就像你做的那样。请注意,variables的范围比发布的代码限制得多。我认为这有助于减轻精神负担。来自外部作用域的隐藏名称在发布的代码中,一些名称隐藏了外部作用域的名称,例如:def get_workspaces_and_variables(行):^ def workspaces_without_variables(工作区、变量):^由于这个原因,我曾经在过去经历过一些讨厌的错误,我建议避免这样的阴影。可用性我本来想玩一下这个程序,但是用硬编码的输入和输出文件路径,这还不够容易。最好接受这些参数作为命令行参数,并可能使用当前值作为默认值。) else name

    # example line: "workspace 1 output $firstMonitor $laptop"
    _, workspace, _, *devices = line.split()
    return resolve(workspace), [resolve(device) for device in devices]

def outputs_to_workspaces(workspaces):
    mappings = [collections.defaultdict(list)
                for _ in range(max(len(outputs) for _, outputs in workspaces))]

    for workspace, outputs in workspaces:
        for index, output in enumerate(outputs):
            mappings[index][output].append(workspace)

    return mappings

def parse_workspaces():
    variables = {}
    workspaces = []

    for line in lines:
        if line.startswith('set'):
            name, value = parse_variable(line)
            variables[name] = value

        elif line.startswith('workspace'):
            workspaces.append(parse_workspace(line, variables))

    return workspaces

return outputs_to_workspaces(parse_workspaces())

这通过了最初的测试用例,所以我认为行为是保留的,即使解析使用了一些简化的逻辑。

注意,要使D13像这样工作,解析工作区行所需的所有变量都已经定义好了,这一点很重要。我不知道i3格式,如果这是一个安全的假设或不。如果没有,那么确实需要第二次传球,就像你做的那样。

请注意,D14的范围比发布的代码限制得多。我认为这有助于减轻精神负担。

来自外部作用域的K115隐藏名称K216

在发布的代码中,一些名称隐藏了外部作用域的名称,例如:

J117

def get_workspaces_and_variables(行):^ def workspaces_without_variables(工作区、变量):^

J218

由于这个原因,我曾经在过去经历过一些讨厌的错误,我建议避免这样的阴影。

K119可用性K220

我本来想玩一下这个程序,但是用硬编码的输入和输出文件路径,这还不够容易。最好接受这些参数作为命令行参数,并可能使用当前值作为默认值。

票数 3
EN

Code Review用户

发布于 2021-10-29 19:06:28

你有医生测试-很好,继续。

最主要的原因是,您有未键入的数据汤。内部数据表示形式不应该类似于JSON。Python (特别是dataclass)使轻量级模型变得简单,这样您就可以获得一些正确性的保证,特别是当您使用mypy时。

指定参数默认值为lines=read_config()是个坏主意。此默认设置将应用于模块加载。如果默认文件是巨大的模块加载将是缓慢的,如果默认文件更改后,从模块负载经过一段时间后,默认将是过时的。拥有默认文件名是可以的,但我不会有这样的整个默认配置。

可以通过使用正则表达式简化startswith解析。

除了doctest之外,在内部有一个带有嵌套函数集合的函数read_workspace_outputs,使得单元测试无法进行。

我对yaml模块不太了解,而且yaml格式非常琐碎,所以直接生成标记并不困难。我对这一点没有强烈的看法,但下面建议的代码显示了直接的标记生成。

您的输出格式有点奇怪;您对它有多少控制?看起来,您是在暗示最外层的树级别是工作区监视器列表中的索引,但是如果您还显示了数字索引本身,它就不会那么令人困惑了。也就是说,下面的示例代码遵循您现有的输出结构。

建议

代码语言:javascript
复制
import re
from dataclasses import dataclass, field
from pathlib import Path
from string import Template
from typing import Iterator, Iterable, Tuple, Dict, List, Collection

CONFIG = Path.home() / ".config" / "i3" / "config"


@dataclass(frozen=True)
class Monitor:
    name: str
    workspaces: Dict[int, 'Workspace'] = field(default_factory=dict)


@dataclass(frozen=True)
class Workspace:
    id: int
    monitors: List[Monitor] = field(default_factory=list)


WorkspaceDefinition = Tuple[
    int,  # Workspace ID
    Tuple[str],  # monitors
]


def read_config(config: Path = CONFIG) -> Iterator[WorkspaceDefinition]:
    variables: Dict[str, str] = {}
    var_pat = re.compile(
        r'^set'
        r' \$(?P\S+)'
        r' (?P.*)'
        r'\n
    )
    work_pat = re.compile(
        r'^workspace'
        r' (?P\S+)'
        r' output'
        r' (?P.+)'
        r'\n
    )

    with config.open() as f:
        for line in f:
            var = var_pat.match(line)
            if var:
                variables[var['key']] = var['value']
                continue

            filled = Template(line).substitute(variables)
            workspace = work_pat.match(filled)
            if workspace:
                yield int(workspace['id']), workspace['monitors'].split(' ')


def load_workspaces(work_defs: Iterable[WorkspaceDefinition]) -> Tuple[
    Tuple[Workspace],
    Tuple[Monitor],
]:
    monitors = {}
    workspaces = {}

    for work_id, monitor_names in work_defs:
        workspace = Workspace(id=work_id)
        workspaces[work_id] = workspace

        for monitor_name in monitor_names:
            monitor = monitors.get(monitor_name)
            if monitor is None:
                monitor = Monitor(name=monitor_name)
                monitors[monitor_name] = monitor
            monitor.workspaces[work_id] = workspace
            workspace.monitors.append(monitor)

    return tuple(workspaces.values()), tuple(monitors.values())


@dataclass(frozen=True)
class MonitorPosition:
    index: int
    workspaces_by_monitor: Dict[str, List[Workspace]]

    @classmethod
    def all(
        cls,
        workspaces: Collection[Workspace],
        monitors: Collection[Monitor],
    ) -> Iterator['MonitorPosition']:
        monitor_positions = max(
            len(workspace.monitors)
            for workspace in workspaces
        )

        for index in range(monitor_positions):
            yield cls(
                index,
                dict(cls.for_index(workspaces, monitors, index)),
            )

    @classmethod
    def for_index(
        cls,
        workspaces: Collection[Workspace],
        monitors: Collection[Monitor],
        index: int,
    ) -> Iterator[Tuple[str, List[Workspace]]]:
        for monitor in monitors:
            workspaces_used = [
                workspace for workspace in workspaces
                if len(workspace.monitors) > index
                and workspace.monitors[index] is monitor
            ]
            if workspaces_used:
                yield monitor.name, workspaces_used


def to_yaml(
    positions: Iterable[MonitorPosition],
    filename: Path,
) -> None:
    with filename.open('w') as yaml:
        for position in positions:
            group_prefix = '-'
            for monitor_name, workspaces in position.workspaces_by_monitor.items():
                yaml.write(f'{group_prefix:<2}{monitor_name}:\n')
                yaml.write('\n'.join(
                    f"  - '{workspace.id}'"
                    for workspace in workspaces
                ))
                yaml.write('\n')
                group_prefix = ' '


def test() -> None:
    workspaces, monitors = load_workspaces(read_config(Path('config')))
    positions = MonitorPosition.all(workspaces, monitors)
    to_yaml(positions, Path('i3-output-workspace-2.yaml'))


if __name__ == '__main__':
    test()
票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/269510

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档