cifter-cli 0.1.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cifter-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: C/C++ の関数実装を抽出する軽量 CLI
5
5
  Keywords: c,c++,cpp,cli,tree-sitter,source-extraction,static-analysis
6
6
  Author: t-kenji
@@ -10,7 +10,6 @@ License-File: LICENSE
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.12
@@ -18,6 +17,7 @@ Classifier: Programming Language :: C
18
17
  Classifier: Programming Language :: C++
19
18
  Classifier: Topic :: Software Development
20
19
  Requires-Dist: pcpp>=1.30
20
+ Requires-Dist: rich>=14.3.3
21
21
  Requires-Dist: tree-sitter>=0.25.2
22
22
  Requires-Dist: tree-sitter-c>=0.24.1
23
23
  Requires-Dist: tree-sitter-cpp>=0.23.4
@@ -40,6 +40,7 @@ Description-Content-Type: text/markdown
40
40
  - 公開サブコマンドは `function` / `flow` / `path` の 3 つです
41
41
  - 出力は元ソースと対応付け可能な行番号付き text です
42
42
  - `-D NAME[=VALUE]` により条件分岐前処理を評価できます
43
+ - 出力先が TTY の場合は既定でシンタックスハイライトします
43
44
 
44
45
  ## Why cifter
45
46
 
@@ -59,14 +60,16 @@ python -m pip install cifter-cli
59
60
  最小確認:
60
61
 
61
62
  ```sh
63
+ cift --version
62
64
  cift --help
65
+ python -m cifter --version
63
66
  python -m cifter --help
64
67
  ```
65
68
 
66
69
  GitHub Release の `wheel` / `sdist` から install することもできます。
67
70
 
68
71
  ```sh
69
- python -m pip install ./cifter_cli-0.1.0-py3-none-any.whl
72
+ python -m pip install ./cifter_cli-0.2.1-py3-none-any.whl
70
73
  ```
71
74
 
72
75
  開発用:
@@ -78,40 +81,99 @@ uv run cift --help
78
81
 
79
82
  ## Quick Start
80
83
 
81
- サンプルソース:
84
+ サンプルソース `foo.c`:
82
85
 
83
86
  ```c
84
- int FooFunction(int x)
87
+ int DecideState(int x)
85
88
  {
89
+ int state = 0;
90
+ LogStart();
91
+
86
92
  if (x > 0) {
87
- return 1;
93
+ Prepare();
94
+ state = 1;
95
+ } else {
96
+ PrepareFallback();
97
+ state = 2;
88
98
  }
89
99
 
90
- return 0;
100
+ Finalize();
101
+ return state;
91
102
  }
92
103
  ```
93
104
 
94
- 関数全体を抽出:
105
+ まず全体を確認したいときは、関数をそのまま抜きます。
106
+
107
+ ```sh
108
+ cift function --name DecideState --source foo.c
109
+ ```
110
+
111
+ ```text
112
+ 1: int DecideState(int x)
113
+ 2: {
114
+ 3: int state = 0;
115
+ 4: LogStart();
116
+ 5:
117
+ 6: if (x > 0) {
118
+ 7: Prepare();
119
+ 8: state = 1;
120
+ 9: } else {
121
+ 10: PrepareFallback();
122
+ 11: state = 2;
123
+ 12: }
124
+ 13:
125
+ 14: Finalize();
126
+ 15: return state;
127
+ 16: }
128
+ ```
129
+
130
+ 分岐の骨格と、見たい値更新だけを薄く追いたいときは `flow` を使います。
95
131
 
96
132
  ```sh
97
- cift function --name FooFunction --source foo.c
133
+ cift flow --function DecideState --source foo.c --track state
134
+ ```
135
+
136
+ ```text
137
+ 1: int DecideState(int x)
138
+ 2: {
139
+ 3: int state = 0;
140
+ 6: if (x > 0) {
141
+ 8: state = 1;
142
+ 9: } else {
143
+ 11: state = 2;
144
+ 12: }
145
+ 15: return state;
146
+ 16: }
98
147
  ```
99
148
 
100
- 出力:
149
+ 失敗側や `else` 側の 1 本だけを追って、その後に何が起きるかまで見たいときは `path` を使います。
150
+
151
+ ```sh
152
+ cift path --function DecideState --source foo.c --route 'else'
153
+ ```
101
154
 
102
155
  ```text
103
- 1: int FooFunction(int x)
156
+ 1: int DecideState(int x)
104
157
  2: {
105
- 3: if (x > 0) {
106
- 4: return 1;
107
- 5: }
108
- 6:
109
- 7: return 0;
110
- 8: }
158
+ 6: if (x > 0) {
159
+ 9: } else {
160
+ 10: PrepareFallback();
161
+ 11: state = 2;
162
+ 12: }
163
+ 14: Finalize();
164
+ 15: return state;
165
+ 16: }
111
166
  ```
112
167
 
113
168
  ## Commands
114
169
 
170
+ バージョン表示:
171
+
172
+ ```sh
173
+ cift --version
174
+ python -m cifter --version
175
+ ```
176
+
115
177
  `function`:
116
178
  指定した関数の実装全体をそのまま抽出します。レビュー対象の最小切り出しに向きます。
117
179
 
@@ -178,6 +240,8 @@ cift function --name FooFunction --source examples/demo.c -D DEF_FOO -D ENABLE_B
178
240
 
179
241
  ```sh
180
242
  cift function --name FooFunction --source examples/demo.c
243
+ cift function --name FooFunction --source examples/demo.c --color
244
+ cift function --name FooFunction --source examples/demo.c --no-color
181
245
  cift flow --function FooFunction --source examples/demo.c --track 'ctx->state'
182
246
  cift path --function FooFunction --source examples/demo.c --route 'case CMD_LOOP > if ret == OK'
183
247
  ```
@@ -9,6 +9,7 @@
9
9
  - 公開サブコマンドは `function` / `flow` / `path` の 3 つです
10
10
  - 出力は元ソースと対応付け可能な行番号付き text です
11
11
  - `-D NAME[=VALUE]` により条件分岐前処理を評価できます
12
+ - 出力先が TTY の場合は既定でシンタックスハイライトします
12
13
 
13
14
  ## Why cifter
14
15
 
@@ -28,14 +29,16 @@ python -m pip install cifter-cli
28
29
  最小確認:
29
30
 
30
31
  ```sh
32
+ cift --version
31
33
  cift --help
34
+ python -m cifter --version
32
35
  python -m cifter --help
33
36
  ```
34
37
 
35
38
  GitHub Release の `wheel` / `sdist` から install することもできます。
36
39
 
37
40
  ```sh
38
- python -m pip install ./cifter_cli-0.1.0-py3-none-any.whl
41
+ python -m pip install ./cifter_cli-0.2.1-py3-none-any.whl
39
42
  ```
40
43
 
41
44
  開発用:
@@ -47,40 +50,99 @@ uv run cift --help
47
50
 
48
51
  ## Quick Start
49
52
 
50
- サンプルソース:
53
+ サンプルソース `foo.c`:
51
54
 
52
55
  ```c
53
- int FooFunction(int x)
56
+ int DecideState(int x)
54
57
  {
58
+ int state = 0;
59
+ LogStart();
60
+
55
61
  if (x > 0) {
56
- return 1;
62
+ Prepare();
63
+ state = 1;
64
+ } else {
65
+ PrepareFallback();
66
+ state = 2;
57
67
  }
58
68
 
59
- return 0;
69
+ Finalize();
70
+ return state;
60
71
  }
61
72
  ```
62
73
 
63
- 関数全体を抽出:
74
+ まず全体を確認したいときは、関数をそのまま抜きます。
75
+
76
+ ```sh
77
+ cift function --name DecideState --source foo.c
78
+ ```
79
+
80
+ ```text
81
+ 1: int DecideState(int x)
82
+ 2: {
83
+ 3: int state = 0;
84
+ 4: LogStart();
85
+ 5:
86
+ 6: if (x > 0) {
87
+ 7: Prepare();
88
+ 8: state = 1;
89
+ 9: } else {
90
+ 10: PrepareFallback();
91
+ 11: state = 2;
92
+ 12: }
93
+ 13:
94
+ 14: Finalize();
95
+ 15: return state;
96
+ 16: }
97
+ ```
98
+
99
+ 分岐の骨格と、見たい値更新だけを薄く追いたいときは `flow` を使います。
64
100
 
65
101
  ```sh
66
- cift function --name FooFunction --source foo.c
102
+ cift flow --function DecideState --source foo.c --track state
103
+ ```
104
+
105
+ ```text
106
+ 1: int DecideState(int x)
107
+ 2: {
108
+ 3: int state = 0;
109
+ 6: if (x > 0) {
110
+ 8: state = 1;
111
+ 9: } else {
112
+ 11: state = 2;
113
+ 12: }
114
+ 15: return state;
115
+ 16: }
67
116
  ```
68
117
 
69
- 出力:
118
+ 失敗側や `else` 側の 1 本だけを追って、その後に何が起きるかまで見たいときは `path` を使います。
119
+
120
+ ```sh
121
+ cift path --function DecideState --source foo.c --route 'else'
122
+ ```
70
123
 
71
124
  ```text
72
- 1: int FooFunction(int x)
125
+ 1: int DecideState(int x)
73
126
  2: {
74
- 3: if (x > 0) {
75
- 4: return 1;
76
- 5: }
77
- 6:
78
- 7: return 0;
79
- 8: }
127
+ 6: if (x > 0) {
128
+ 9: } else {
129
+ 10: PrepareFallback();
130
+ 11: state = 2;
131
+ 12: }
132
+ 14: Finalize();
133
+ 15: return state;
134
+ 16: }
80
135
  ```
81
136
 
82
137
  ## Commands
83
138
 
139
+ バージョン表示:
140
+
141
+ ```sh
142
+ cift --version
143
+ python -m cifter --version
144
+ ```
145
+
84
146
  `function`:
85
147
  指定した関数の実装全体をそのまま抽出します。レビュー対象の最小切り出しに向きます。
86
148
 
@@ -147,6 +209,8 @@ cift function --name FooFunction --source examples/demo.c -D DEF_FOO -D ENABLE_B
147
209
 
148
210
  ```sh
149
211
  cift function --name FooFunction --source examples/demo.c
212
+ cift function --name FooFunction --source examples/demo.c --color
213
+ cift function --name FooFunction --source examples/demo.c --no-color
150
214
  cift flow --function FooFunction --source examples/demo.c --track 'ctx->state'
151
215
  cift path --function FooFunction --source examples/demo.c --route 'case CMD_LOOP > if ret == OK'
152
216
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cifter-cli"
3
- version = "0.1.0"
3
+ version = "0.2.1"
4
4
  description = "C/C++ の関数実装を抽出する軽量 CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,7 +22,6 @@ classifiers = [
22
22
  "Development Status :: 3 - Alpha",
23
23
  "Environment :: Console",
24
24
  "Intended Audience :: Developers",
25
- "License :: OSI Approved :: MIT License",
26
25
  "Operating System :: OS Independent",
27
26
  "Programming Language :: Python :: 3",
28
27
  "Programming Language :: Python :: 3.12",
@@ -32,6 +31,7 @@ classifiers = [
32
31
  ]
33
32
  dependencies = [
34
33
  "pcpp>=1.30",
34
+ "rich>=14.3.3",
35
35
  "tree-sitter>=0.25.2",
36
36
  "tree-sitter-c>=0.24.1",
37
37
  "tree-sitter-cpp>=0.23.4",
@@ -0,0 +1,6 @@
1
+ from cifter.cli import main
2
+ from cifter.version import get_version
3
+
4
+ __version__ = get_version()
5
+
6
+ __all__ = ["__version__", "main"]
@@ -10,9 +10,10 @@ from cifter.errors import CiftError
10
10
  from cifter.extract_flow import extract_flow
11
11
  from cifter.extract_function import extract_function
12
12
  from cifter.extract_path import extract_path
13
- from cifter.model import TrackPath
13
+ from cifter.model import ExtractionResult, TrackPath
14
14
  from cifter.parser import parse_source
15
- from cifter.render import render_result
15
+ from cifter.render import print_result
16
+ from cifter.version import format_version_output
16
17
 
17
18
  app = typer.Typer(no_args_is_help=True, help="C/C++ の関数実装を抽出する CLI")
18
19
 
@@ -21,6 +22,28 @@ def main() -> None:
21
22
  app()
22
23
 
23
24
 
25
+ def _version_callback(value: bool) -> None:
26
+ if not value:
27
+ return
28
+ typer.echo(format_version_output())
29
+ raise typer.Exit()
30
+
31
+
32
+ @app.callback()
33
+ def common_options(
34
+ version: Annotated[
35
+ bool,
36
+ typer.Option(
37
+ "--version",
38
+ callback=_version_callback,
39
+ is_eager=True,
40
+ help="バージョンを表示して終了する",
41
+ ),
42
+ ] = False,
43
+ ) -> None:
44
+ _ = version
45
+
46
+
24
47
  SourceOption = Annotated[
25
48
  Path,
26
49
  typer.Option(
@@ -34,17 +57,30 @@ SourceOption = Annotated[
34
57
  ),
35
58
  ]
36
59
 
60
+ ColorOption = Annotated[
61
+ bool | None,
62
+ typer.Option(
63
+ "--color/--no-color",
64
+ help="抽出結果のシンタックスハイライトを制御する",
65
+ ),
66
+ ]
67
+
37
68
 
38
69
  @app.command("function")
39
70
  def function_command(
40
71
  name: Annotated[str, typer.Option("--name", help="抽出する関数名")],
41
72
  source: SourceOption,
73
+ color: ColorOption = None,
42
74
  defines: Annotated[
43
75
  list[str] | None,
44
76
  typer.Option("--define", "-D", help="条件分岐評価に使うマクロ定義"),
45
77
  ] = None,
46
78
  ) -> None:
47
- _run(lambda: render_result(extract_function(parse_source(source, defines or []), name)))
79
+ def task() -> tuple[ExtractionResult, str]:
80
+ parsed = parse_source(source, defines or [])
81
+ return extract_function(parsed, name), parsed.language_name
82
+
83
+ _run(task, color=color)
48
84
 
49
85
 
50
86
  @app.command("flow")
@@ -52,17 +88,18 @@ def flow_command(
52
88
  function_name: Annotated[str, typer.Option("--function", help="対象関数名")],
53
89
  source: SourceOption,
54
90
  track: Annotated[list[str], typer.Option("--track", help="保持するアクセスパス")] | None = None,
91
+ color: ColorOption = None,
55
92
  defines: Annotated[
56
93
  list[str] | None,
57
94
  typer.Option("--define", "-D", help="条件分岐評価に使うマクロ定義"),
58
95
  ] = None,
59
96
  ) -> None:
60
- def task() -> str:
97
+ def task() -> tuple[ExtractionResult, str]:
61
98
  parsed = parse_source(source, defines or [])
62
99
  tracks = tuple(TrackPath.parse(value) for value in (track or []))
63
- return render_result(extract_flow(parsed, function_name, tracks))
100
+ return extract_flow(parsed, function_name, tracks), parsed.language_name
64
101
 
65
- _run(task)
102
+ _run(task, color=color)
66
103
 
67
104
 
68
105
  @app.command("path")
@@ -70,21 +107,23 @@ def path_command(
70
107
  function_name: Annotated[str, typer.Option("--function", help="対象関数名")],
71
108
  source: SourceOption,
72
109
  route: Annotated[str, typer.Option("--route", help="抽出する経路 DSL")],
110
+ color: ColorOption = None,
73
111
  defines: Annotated[
74
112
  list[str] | None,
75
113
  typer.Option("--define", "-D", help="条件分岐評価に使うマクロ定義"),
76
114
  ] = None,
77
115
  ) -> None:
78
- def task() -> str:
116
+ def task() -> tuple[ExtractionResult, str]:
79
117
  parsed = parse_source(source, defines or [])
80
- return render_result(extract_path(parsed, function_name, route))
118
+ return extract_path(parsed, function_name, route), parsed.language_name
81
119
 
82
- _run(task)
120
+ _run(task, color=color)
83
121
 
84
122
 
85
- def _run(task: Callable[[], str]) -> None:
123
+ def _run(task: Callable[[], tuple[ExtractionResult, str]], *, color: bool | None) -> None:
86
124
  try:
87
- typer.echo(task())
125
+ result, language_name = task()
126
+ print_result(result, language_name, color=color)
88
127
  except CiftError as error:
89
128
  typer.echo(error.message, err=True)
90
129
  raise typer.Exit(code=1) from error
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from io import StringIO
5
+ from typing import TextIO
6
+
7
+ from rich.console import Console
8
+ from rich.syntax import Syntax
9
+ from rich.text import Text
10
+
11
+ from cifter.model import ExtractionResult
12
+
13
+
14
+ def render_result(result: ExtractionResult) -> str:
15
+ width = len(str(result.lines[-1].line_no))
16
+ return "\n".join(f"{line.line_no:>{width}}: {line.text}" for line in result.lines)
17
+
18
+
19
+ def print_result(
20
+ result: ExtractionResult,
21
+ language_name: str,
22
+ *,
23
+ color: bool | None,
24
+ file: TextIO | None = None,
25
+ ) -> None:
26
+ output = file or sys.stdout
27
+ if not _should_use_color(color, output):
28
+ output.write(f"{render_result(result)}\n")
29
+ return
30
+
31
+ console = Console(
32
+ file=StringIO(),
33
+ record=True,
34
+ force_terminal=True,
35
+ color_system="truecolor",
36
+ )
37
+ syntax = Syntax("", language_name)
38
+ width = len(str(result.lines[-1].line_no))
39
+ for line in result.lines:
40
+ rendered_line = Text.assemble(
41
+ (f"{line.line_no:>{width}}: ", "dim"),
42
+ syntax.highlight(line.text)[:-1],
43
+ )
44
+ console.print(rendered_line)
45
+ output.write(console.export_text(styles=True))
46
+
47
+
48
+ def _should_use_color(color: bool | None, file: TextIO) -> bool:
49
+ if color is not None:
50
+ return color
51
+ isatty = getattr(file, "isatty", None)
52
+ if not callable(isatty):
53
+ return False
54
+ return bool(isatty())
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from pathlib import Path
6
+
7
+ PACKAGE_NAME = "cifter-cli"
8
+ COMMAND_NAME = "cift"
9
+
10
+
11
+ def get_version() -> str:
12
+ try:
13
+ return version(PACKAGE_NAME)
14
+ except PackageNotFoundError:
15
+ return _read_version_from_pyproject()
16
+
17
+
18
+ def format_version_output() -> str:
19
+ return f"{COMMAND_NAME} {get_version()}"
20
+
21
+
22
+ def _read_version_from_pyproject() -> str:
23
+ pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
24
+ data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
25
+ project = data.get("project")
26
+ if not isinstance(project, dict):
27
+ raise RuntimeError("pyproject.toml に project セクションがありません")
28
+ version_value = project.get("version")
29
+ if not isinstance(version_value, str):
30
+ raise RuntimeError("pyproject.toml の project.version が文字列ではありません")
31
+ return version_value
@@ -1,3 +0,0 @@
1
- from cifter.cli import main
2
-
3
- __all__ = ["main"]
@@ -1,8 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from cifter.model import ExtractionResult
4
-
5
-
6
- def render_result(result: ExtractionResult) -> str:
7
- width = len(str(result.lines[-1].line_no))
8
- return "\n".join(f"{line.line_no:>{width}}: {line.text}" for line in result.lines)
File without changes