stata-cli 0.2.2__tar.gz → 0.3.0__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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stata-cli
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Command-line interface for running Stata commands via PyStata
5
5
  License: MIT
6
6
  Keywords: stata,cli,statistics,data-science
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  Requires-Dist: click>=8.0
10
10
  Provides-Extra: data
@@ -13,8 +13,10 @@ Requires-Dist: pandas; extra == "data"
13
13
 
14
14
  # stata-cli
15
15
 
16
+ ![stata-cli banner](assets/banner.png)
17
+
16
18
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
- [![Python Version](https://img.shields.io/badge/python-%3E%3D3.10-blue.svg)](https://www.python.org/)
19
+ [![Python Version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://www.python.org/)
18
20
  [![npm version](https://img.shields.io/npm/v/stata-cli.svg)](https://www.npmjs.com/package/stata-cli)
19
21
 
20
22
  [中文版](README.zh.md) | [English](README.md)
@@ -39,10 +41,16 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
39
41
  | **Run Code** | Execute inline Stata code, multi-line blocks, or pipe from stdin |
40
42
  | **Do Files** | Run `.do` files with `///` line continuation support and graph auto-naming |
41
43
  | **Data Viewer** | View current dataset as JSON with `if`-condition filtering and row limits |
44
+ | **Variable Metadata** | Inspect variable names, types, formats, and labels via `vars` |
45
+ | **Stored Results** | Retrieve r(), e(), s() results as structured JSON via `return` |
46
+ | **Matrix Access** | Read Stata matrices (e.g. `e(b)`, `e(V)`) as JSON via `matrix` |
47
+ | **Value Labels** | List and inspect value labels via `labels` |
48
+ | **Macro Access** | Get/set Stata macros including `c()`, `e()`, `r()` system macros |
49
+ | **Frame Management** | List Stata frames and current working frame via `frame` |
42
50
  | **Help System** | Browse Stata help topics with SMCL-to-plain-text conversion |
43
- | **Graph Export** | Auto-detect and export graphs as PNG to `~/.stata-cli/graphs/` |
51
+ | **Graph Export** | Auto-detect and export graphs as PNG/SVG/PDF to `~/.stata-cli/graphs/` |
44
52
  | **Daemon Mode** | Persistent background process for sub-second execution via Unix socket |
45
- | **Output Control** | Compact mode, JSON output, token limit management with full-output file save |
53
+ | **Output Control** | Compact mode, JSON output, token limit management, log file output |
46
54
  | **Interruption** | Send break signal to stop long-running commands |
47
55
 
48
56
  ## Installation & Quick Start
@@ -50,7 +58,7 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
50
58
  ### Requirements
51
59
 
52
60
  - **Stata 17+** installed on your machine (provides the PyStata library)
53
- - Python 3.10+
61
+ - Python 3.9+
54
62
 
55
63
  ### Quick Start (Human Users)
56
64
 
@@ -197,6 +205,60 @@ stata-cli detect
197
205
 
198
206
  Prints the auto-detected Stata installation path.
199
207
 
208
+ ### `return` — Retrieve Stored Results
209
+
210
+ ```bash
211
+ stata-cli return r # r() results (after summarize, etc.)
212
+ stata-cli return e # e() results (after regress, etc.)
213
+ stata-cli return s # s() results
214
+ ```
215
+
216
+ Returns r(), e(), or s() stored results as structured JSON — scalars, macros, and matrix references.
217
+
218
+ ### `vars` — Variable Metadata
219
+
220
+ ```bash
221
+ stata-cli vars # all variables
222
+ stata-cli vars price mpg # specific variables
223
+ ```
224
+
225
+ Returns variable names, types, formats, and labels as JSON. More structured than `describe`.
226
+
227
+ ### `matrix` — Read Stata Matrices
228
+
229
+ ```bash
230
+ stata-cli matrix e(b) # coefficient vector
231
+ stata-cli matrix e(V) # variance-covariance matrix
232
+ ```
233
+
234
+ Returns matrix data, dimensions, and row/column names as JSON.
235
+
236
+ ### `labels` — Value Labels
237
+
238
+ ```bash
239
+ stata-cli labels # list all value label names
240
+ stata-cli labels origin # show value-label mapping
241
+ stata-cli labels --var foreign # show label attached to a variable
242
+ ```
243
+
244
+ ### `macro` — Get/Set Macros
245
+
246
+ ```bash
247
+ stata-cli macro get "c(current_date)"
248
+ stata-cli macro get "e(cmd)"
249
+ stata-cli macro set myvar "hello"
250
+ ```
251
+
252
+ Access Stata macros including system macros (`c()`, `e()`, `r()`).
253
+
254
+ ### `frame` — List Frames
255
+
256
+ ```bash
257
+ stata-cli frame
258
+ ```
259
+
260
+ Shows all Stata frames and the current working frame.
261
+
200
262
  ## Daemon Mode
201
263
 
202
264
  The daemon keeps PyStata alive in the background — reduces execution time from **~2-3s to ~85ms** (35x speedup).
@@ -234,6 +296,8 @@ The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle
234
296
  | `--max-tokens N` | Max output tokens (0=unlimited) | 0 |
235
297
  | `--no-daemon` | Force direct execution | off |
236
298
  | `--graphs-dir PATH` | Graph export directory | `~/.stata-cli/graphs/` |
299
+ | `--graph-format [png\|svg\|pdf]` | Graph export format | `png` |
300
+ | `--log PATH` | Save output to a log file | off |
237
301
 
238
302
  ### JSON Output
239
303
 
@@ -313,6 +377,21 @@ regress price mpg weight
313
377
  predict yhat
314
378
  list make price yhat in 1/5"
315
379
 
380
+ # Retrieve regression results as structured JSON
381
+ stata-cli return e
382
+
383
+ # Get coefficient matrix
384
+ stata-cli matrix e(b)
385
+
386
+ # Inspect variable metadata
387
+ stata-cli vars price mpg weight
388
+
389
+ # Check value labels
390
+ stata-cli labels --var foreign
391
+
392
+ # Read system macros
393
+ stata-cli macro get "c(N)"
394
+
316
395
  # Check data after loading
317
396
  stata-cli data --if "price>10000"
318
397
 
@@ -325,6 +404,9 @@ describe"
325
404
 
326
405
  # JSON mode for structured parsing
327
406
  stata-cli --json run "display 1+1"
407
+
408
+ # Export graph as SVG
409
+ stata-cli --graph-format svg run "scatter price mpg"
328
410
  ```
329
411
 
330
412
  ## Contributing
@@ -1,7 +1,9 @@
1
1
  # stata-cli
2
2
 
3
+ ![stata-cli banner](assets/banner.png)
4
+
3
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![Python Version](https://img.shields.io/badge/python-%3E%3D3.10-blue.svg)](https://www.python.org/)
6
+ [![Python Version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://www.python.org/)
5
7
  [![npm version](https://img.shields.io/npm/v/stata-cli.svg)](https://www.npmjs.com/package/stata-cli)
6
8
 
7
9
  [中文版](README.zh.md) | [English](README.md)
@@ -26,10 +28,16 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
26
28
  | **Run Code** | Execute inline Stata code, multi-line blocks, or pipe from stdin |
27
29
  | **Do Files** | Run `.do` files with `///` line continuation support and graph auto-naming |
28
30
  | **Data Viewer** | View current dataset as JSON with `if`-condition filtering and row limits |
31
+ | **Variable Metadata** | Inspect variable names, types, formats, and labels via `vars` |
32
+ | **Stored Results** | Retrieve r(), e(), s() results as structured JSON via `return` |
33
+ | **Matrix Access** | Read Stata matrices (e.g. `e(b)`, `e(V)`) as JSON via `matrix` |
34
+ | **Value Labels** | List and inspect value labels via `labels` |
35
+ | **Macro Access** | Get/set Stata macros including `c()`, `e()`, `r()` system macros |
36
+ | **Frame Management** | List Stata frames and current working frame via `frame` |
29
37
  | **Help System** | Browse Stata help topics with SMCL-to-plain-text conversion |
30
- | **Graph Export** | Auto-detect and export graphs as PNG to `~/.stata-cli/graphs/` |
38
+ | **Graph Export** | Auto-detect and export graphs as PNG/SVG/PDF to `~/.stata-cli/graphs/` |
31
39
  | **Daemon Mode** | Persistent background process for sub-second execution via Unix socket |
32
- | **Output Control** | Compact mode, JSON output, token limit management with full-output file save |
40
+ | **Output Control** | Compact mode, JSON output, token limit management, log file output |
33
41
  | **Interruption** | Send break signal to stop long-running commands |
34
42
 
35
43
  ## Installation & Quick Start
@@ -37,7 +45,7 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
37
45
  ### Requirements
38
46
 
39
47
  - **Stata 17+** installed on your machine (provides the PyStata library)
40
- - Python 3.10+
48
+ - Python 3.9+
41
49
 
42
50
  ### Quick Start (Human Users)
43
51
 
@@ -184,6 +192,60 @@ stata-cli detect
184
192
 
185
193
  Prints the auto-detected Stata installation path.
186
194
 
195
+ ### `return` — Retrieve Stored Results
196
+
197
+ ```bash
198
+ stata-cli return r # r() results (after summarize, etc.)
199
+ stata-cli return e # e() results (after regress, etc.)
200
+ stata-cli return s # s() results
201
+ ```
202
+
203
+ Returns r(), e(), or s() stored results as structured JSON — scalars, macros, and matrix references.
204
+
205
+ ### `vars` — Variable Metadata
206
+
207
+ ```bash
208
+ stata-cli vars # all variables
209
+ stata-cli vars price mpg # specific variables
210
+ ```
211
+
212
+ Returns variable names, types, formats, and labels as JSON. More structured than `describe`.
213
+
214
+ ### `matrix` — Read Stata Matrices
215
+
216
+ ```bash
217
+ stata-cli matrix e(b) # coefficient vector
218
+ stata-cli matrix e(V) # variance-covariance matrix
219
+ ```
220
+
221
+ Returns matrix data, dimensions, and row/column names as JSON.
222
+
223
+ ### `labels` — Value Labels
224
+
225
+ ```bash
226
+ stata-cli labels # list all value label names
227
+ stata-cli labels origin # show value-label mapping
228
+ stata-cli labels --var foreign # show label attached to a variable
229
+ ```
230
+
231
+ ### `macro` — Get/Set Macros
232
+
233
+ ```bash
234
+ stata-cli macro get "c(current_date)"
235
+ stata-cli macro get "e(cmd)"
236
+ stata-cli macro set myvar "hello"
237
+ ```
238
+
239
+ Access Stata macros including system macros (`c()`, `e()`, `r()`).
240
+
241
+ ### `frame` — List Frames
242
+
243
+ ```bash
244
+ stata-cli frame
245
+ ```
246
+
247
+ Shows all Stata frames and the current working frame.
248
+
187
249
  ## Daemon Mode
188
250
 
189
251
  The daemon keeps PyStata alive in the background — reduces execution time from **~2-3s to ~85ms** (35x speedup).
@@ -221,6 +283,8 @@ The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle
221
283
  | `--max-tokens N` | Max output tokens (0=unlimited) | 0 |
222
284
  | `--no-daemon` | Force direct execution | off |
223
285
  | `--graphs-dir PATH` | Graph export directory | `~/.stata-cli/graphs/` |
286
+ | `--graph-format [png\|svg\|pdf]` | Graph export format | `png` |
287
+ | `--log PATH` | Save output to a log file | off |
224
288
 
225
289
  ### JSON Output
226
290
 
@@ -300,6 +364,21 @@ regress price mpg weight
300
364
  predict yhat
301
365
  list make price yhat in 1/5"
302
366
 
367
+ # Retrieve regression results as structured JSON
368
+ stata-cli return e
369
+
370
+ # Get coefficient matrix
371
+ stata-cli matrix e(b)
372
+
373
+ # Inspect variable metadata
374
+ stata-cli vars price mpg weight
375
+
376
+ # Check value labels
377
+ stata-cli labels --var foreign
378
+
379
+ # Read system macros
380
+ stata-cli macro get "c(N)"
381
+
303
382
  # Check data after loading
304
383
  stata-cli data --if "price>10000"
305
384
 
@@ -312,6 +391,9 @@ describe"
312
391
 
313
392
  # JSON mode for structured parsing
314
393
  stata-cli --json run "display 1+1"
394
+
395
+ # Export graph as SVG
396
+ stata-cli --graph-format svg run "scatter price mpg"
315
397
  ```
316
398
 
317
399
  ## Contributing
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "stata-cli"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "Command-line interface for running Stata commands via PyStata"
5
5
  readme = "README.md"
6
- requires-python = ">=3.10"
6
+ requires-python = ">=3.9"
7
7
  license = {text = "MIT"}
8
8
  keywords = ["stata", "cli", "statistics", "data-science"]
9
9
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -162,6 +162,37 @@ class DaemonServer:
162
162
  ok = self._engine.stop()
163
163
  _send_msg(conn, {"status": "ok" if ok else "no_op"})
164
164
 
165
+ elif cmd_type == "get_return":
166
+ data = self._engine.get_return(rtype=payload.get("rtype", "r"))
167
+ _send_msg(conn, data)
168
+
169
+ elif cmd_type == "get_vars":
170
+ data = self._engine.get_vars()
171
+ _send_msg(conn, data)
172
+
173
+ elif cmd_type == "get_matrix":
174
+ data = self._engine.get_matrix(payload.get("name", ""))
175
+ _send_msg(conn, data)
176
+
177
+ elif cmd_type == "get_labels":
178
+ data = self._engine.get_labels(
179
+ name=payload.get("name"),
180
+ var=payload.get("var"),
181
+ )
182
+ _send_msg(conn, data)
183
+
184
+ elif cmd_type == "get_macro":
185
+ data = self._engine.get_macro(payload.get("name", ""))
186
+ _send_msg(conn, data)
187
+
188
+ elif cmd_type == "set_macro":
189
+ data = self._engine.set_macro(payload.get("name", ""), payload.get("value", ""))
190
+ _send_msg(conn, data)
191
+
192
+ elif cmd_type == "get_frames":
193
+ data = self._engine.get_frames()
194
+ _send_msg(conn, data)
195
+
165
196
  elif cmd_type == "status":
166
197
  _send_msg(conn, {
167
198
  "status": "ok",
@@ -50,14 +50,18 @@ _EXISTING_GRAPHN_RE = re.compile(r"\bname\s*\(\s*graph(\d+)", re.IGNORECASE)
50
50
  class StataEngine:
51
51
  """Thin wrapper around PyStata for single-process command execution."""
52
52
 
53
- def __init__(self, stata_path: str, edition: str = "mp", graphs_dir: Optional[str] = None):
53
+ def __init__(self, stata_path: str, edition: str = "mp", graphs_dir: Optional[str] = None, graph_format: str = "png"):
54
54
  self.stata_path = stata_path
55
55
  self.edition = edition.lower()
56
56
  self.graphs_dir = graphs_dir or get_graphs_root()
57
+ self.graph_format = graph_format.lower()
57
58
  self._stata = None
58
59
  self._stlib = None
59
60
  self._initialized = False
60
61
  self._stop_sent = False
62
+ self._last_r: dict = {}
63
+ self._last_e: dict = {}
64
+ self._last_s: dict = {}
61
65
 
62
66
  def _ensure_initialized(self) -> None:
63
67
  if self._initialized:
@@ -89,6 +93,13 @@ class StataEngine:
89
93
 
90
94
  self._stata = stata_module
91
95
 
96
+ if self.graph_format != "png":
97
+ try:
98
+ from pystata import config as pystata_config # type: ignore[import-untyped]
99
+ pystata_config.set_graph_format(self.graph_format)
100
+ except Exception:
101
+ pass
102
+
92
103
  try:
93
104
  from pystata.config import stlib as stlib_module # type: ignore[import-untyped]
94
105
  self._stlib = stlib_module
@@ -113,19 +124,27 @@ class StataEngine:
113
124
  )
114
125
  log_file_stata = log_file.replace("\\", "/")
115
126
 
116
- wrapped = (
127
+ setup = (
117
128
  f'capture log close _all\n'
118
129
  f'log using "{log_file_stata}", replace text\n'
119
- f'{code}\n'
120
- f'capture log close _all\n'
121
130
  )
131
+ teardown = 'capture log close _all\n'
122
132
 
123
133
  start = time.time()
124
134
  try:
125
135
  old_stdout = sys.stdout
126
136
  sys.stdout = io.StringIO()
127
137
  try:
128
- self._stata.run(wrapped, echo=True, inline=False)
138
+ self._stata.run(setup + code, echo=True, inline=False)
139
+ finally:
140
+ sys.stdout = old_stdout
141
+
142
+ self._capture_stored_results()
143
+
144
+ old_stdout = sys.stdout
145
+ sys.stdout = io.StringIO()
146
+ try:
147
+ self._stata.run(teardown, echo=False, inline=False)
129
148
  finally:
130
149
  captured_stdout = sys.stdout.getvalue()
131
150
  sys.stdout = old_stdout
@@ -300,6 +319,119 @@ class StataEngine:
300
319
 
301
320
  return result
302
321
 
322
+ def get_return(self, rtype: str = "r") -> Dict[str, Any]:
323
+ """Retrieve stored results: r(), e(), or s()."""
324
+ self._ensure_initialized()
325
+ try:
326
+ if rtype == "r":
327
+ return {"status": "success", "type": "r", "results": dict(self._last_r)}
328
+ elif rtype == "e":
329
+ return {"status": "success", "type": "e", "results": dict(self._last_e)}
330
+ elif rtype == "s":
331
+ return {"status": "success", "type": "s", "results": dict(self._last_s)}
332
+ else:
333
+ return {"status": "error", "error": f"Unknown return type: {rtype}"}
334
+ except Exception as exc:
335
+ return {"status": "error", "error": str(exc)}
336
+
337
+ def get_vars(self) -> Dict[str, Any]:
338
+ """Return variable metadata for the current dataset."""
339
+ self._ensure_initialized()
340
+ try:
341
+ import sfi # type: ignore[import-untyped]
342
+ nvar = sfi.Data.getVarCount()
343
+ nobs = sfi.Data.getObsTotal()
344
+ variables = []
345
+ for i in range(nvar):
346
+ name = sfi.Data.getVarName(i)
347
+ variables.append({
348
+ "name": name,
349
+ "type": sfi.Data.getVarType(i),
350
+ "format": sfi.Data.getVarFormat(i),
351
+ "label": sfi.Data.getVarLabel(i),
352
+ "is_string": sfi.Data.isVarTypeStr(i),
353
+ })
354
+ return {
355
+ "status": "success",
356
+ "n_vars": nvar,
357
+ "n_obs": nobs,
358
+ "variables": variables,
359
+ }
360
+ except Exception as exc:
361
+ return {"status": "error", "error": str(exc)}
362
+
363
+ def get_matrix(self, name: str) -> Dict[str, Any]:
364
+ """Return a Stata matrix as a dict."""
365
+ self._ensure_initialized()
366
+ try:
367
+ import sfi # type: ignore[import-untyped]
368
+ nrows = sfi.Matrix.getRowTotal(name)
369
+ ncols = sfi.Matrix.getColTotal(name)
370
+ row_names = sfi.Matrix.getRowNames(name)
371
+ col_names = sfi.Matrix.getColNames(name)
372
+ data = sfi.Matrix.get(name)
373
+ return {
374
+ "status": "success",
375
+ "name": name,
376
+ "rows": nrows,
377
+ "cols": ncols,
378
+ "row_names": row_names,
379
+ "col_names": col_names,
380
+ "data": data,
381
+ }
382
+ except Exception as exc:
383
+ return {"status": "error", "error": str(exc)}
384
+
385
+ def get_labels(self, name: Optional[str] = None, var: Optional[str] = None) -> Dict[str, Any]:
386
+ """Return value labels."""
387
+ self._ensure_initialized()
388
+ try:
389
+ import sfi # type: ignore[import-untyped]
390
+ if var:
391
+ label_name = sfi.ValueLabel.getVarValueLabel(var)
392
+ if not label_name:
393
+ return {"status": "success", "variable": var, "label_name": "", "labels": {}}
394
+ mapping = sfi.ValueLabel.getValueLabels(label_name)
395
+ return {"status": "success", "variable": var, "label_name": label_name, "labels": mapping}
396
+ if name:
397
+ mapping = sfi.ValueLabel.getValueLabels(name)
398
+ return {"status": "success", "name": name, "labels": mapping}
399
+ names = sfi.ValueLabel.getNames()
400
+ return {"status": "success", "names": names}
401
+ except Exception as exc:
402
+ return {"status": "error", "error": str(exc)}
403
+
404
+ def get_macro(self, name: str) -> Dict[str, Any]:
405
+ """Get the value of a Stata macro."""
406
+ self._ensure_initialized()
407
+ try:
408
+ import sfi # type: ignore[import-untyped]
409
+ value = sfi.Macro.getGlobal(name)
410
+ return {"status": "success", "name": name, "value": value}
411
+ except Exception as exc:
412
+ return {"status": "error", "error": str(exc)}
413
+
414
+ def set_macro(self, name: str, value: str) -> Dict[str, Any]:
415
+ """Set a Stata global macro."""
416
+ self._ensure_initialized()
417
+ try:
418
+ import sfi # type: ignore[import-untyped]
419
+ sfi.Macro.setGlobal(name, value)
420
+ return {"status": "success", "name": name, "value": value}
421
+ except Exception as exc:
422
+ return {"status": "error", "error": str(exc)}
423
+
424
+ def get_frames(self) -> Dict[str, Any]:
425
+ """Return list of Stata frames and the current working frame."""
426
+ self._ensure_initialized()
427
+ try:
428
+ import sfi # type: ignore[import-untyped]
429
+ frames = sfi.Frame.getFrames()
430
+ cwf = sfi.Frame.getCWF()
431
+ return {"status": "success", "frames": frames, "current": cwf}
432
+ except Exception as exc:
433
+ return {"status": "error", "error": str(exc)}
434
+
303
435
  def stop(self) -> bool:
304
436
  """Interrupt a running Stata command. Returns True if signal sent."""
305
437
  if self._stop_sent or self._stlib is None:
@@ -316,6 +448,44 @@ class StataEngine:
316
448
 
317
449
  # ── graph detection ──────────────────────────────────────────────────
318
450
 
451
+ def _capture_stored_results(self) -> None:
452
+ """Snapshot r(), e(), s() results via sfi before log close clears them."""
453
+ try:
454
+ import sfi # type: ignore[import-untyped]
455
+ except ImportError:
456
+ return
457
+
458
+ for rtype, store in [("r", "_last_r"), ("e", "_last_e"), ("s", "_last_s")]:
459
+ result: Dict[str, Any] = {}
460
+ cat = f"{rtype}()"
461
+ try:
462
+ scalar_names = sfi.SFIToolkit.listReturn(cat, "scalar")
463
+ if scalar_names and scalar_names.strip():
464
+ for name in scalar_names.strip().split():
465
+ try:
466
+ result[name] = sfi.Scalar.getValue(f"{rtype}({name})")
467
+ except Exception:
468
+ pass
469
+
470
+ macro_names = sfi.SFIToolkit.listReturn(cat, "macro")
471
+ if macro_names and macro_names.strip():
472
+ for name in macro_names.strip().split():
473
+ try:
474
+ result[name] = sfi.Macro.getGlobal(f"{rtype}({name})")
475
+ except Exception:
476
+ pass
477
+
478
+ if rtype != "s":
479
+ matrix_names = sfi.SFIToolkit.listReturn(cat, "matrix")
480
+ if matrix_names and matrix_names.strip():
481
+ for name in matrix_names.strip().split():
482
+ result[f"matrix:{name}"] = f"[matrix, use 'stata-cli matrix {rtype}({name})']"
483
+ except Exception:
484
+ pass
485
+ setattr(self, store, result)
486
+
487
+ # ── graph detection (continued) ─────────────────────────────────────
488
+
319
489
  def _reset_graph_tracking(self) -> None:
320
490
  if self._stlib is None:
321
491
  return
@@ -348,12 +518,15 @@ class StataEngine:
348
518
  self._stlib.StataSO_Execute(
349
519
  get_encode_str(f"quietly graph display {gname}"), False
350
520
  )
351
- graph_file = os.path.join(batch["batch_dir"], f"{gname}.png")
521
+ ext = self.graph_format
522
+ graph_file = os.path.join(batch["batch_dir"], f"{gname}.{ext}")
352
523
  graph_file_stata = graph_file.replace("\\", "/")
524
+ fmt_opt = f"as({ext}) " if ext != "png" else ""
525
+ size_opt = "width(800) height(600)" if ext == "png" else ""
353
526
  export_cmd = (
354
527
  f'quietly graph export "{graph_file_stata}", '
355
- f"name({gname}) replace width(800) height(600)"
356
- )
528
+ f"{fmt_opt}name({gname}) replace {size_opt}"
529
+ ).rstrip()
357
530
  rc = self._stlib.StataSO_Execute(get_encode_str(export_cmd), False)
358
531
  if rc != 0:
359
532
  continue
@@ -1,4 +1,5 @@
1
1
  """Stata CLI - run Stata commands from the terminal."""
2
+ from __future__ import annotations
2
3
 
3
4
  import json
4
5
  import os
@@ -36,8 +37,10 @@ def _exit(code: int) -> None:
36
37
  @click.option("--max-tokens", type=int, default=0, help="Max output tokens (0=unlimited). Saves full output to file when exceeded.")
37
38
  @click.option("--no-daemon", is_flag=True, default=False, help="Force direct execution, skip daemon.")
38
39
  @click.option("--graphs-dir", envvar="STATA_CLI_GRAPHS_DIR", default=None, help="Graph export directory.")
40
+ @click.option("--graph-format", type=click.Choice(["png", "svg", "pdf"], case_sensitive=False), default="png", help="Graph export format.")
41
+ @click.option("--log", "log_file", default=None, help="Save Stata output to a log file.")
39
42
  @click.pass_context
40
- def cli(ctx, stata_path, edition, compact, use_json, timeout, max_tokens, no_daemon, graphs_dir):
43
+ def cli(ctx, stata_path, edition, compact, use_json, timeout, max_tokens, no_daemon, graphs_dir, graph_format, log_file):
41
44
  """Command-line interface for Stata."""
42
45
  ctx.ensure_object(dict)
43
46
  ctx.obj["stata_path"] = stata_path
@@ -48,6 +51,8 @@ def cli(ctx, stata_path, edition, compact, use_json, timeout, max_tokens, no_dae
48
51
  ctx.obj["max_tokens"] = max_tokens
49
52
  ctx.obj["no_daemon"] = no_daemon
50
53
  ctx.obj["graphs_dir"] = graphs_dir
54
+ ctx.obj["graph_format"] = graph_format
55
+ ctx.obj["log_file"] = log_file
51
56
 
52
57
 
53
58
  def _get_engine(ctx) -> StataEngine:
@@ -57,7 +62,7 @@ def _get_engine(ctx) -> StataEngine:
57
62
  click.echo("Set --stata-path or the STATA_PATH environment variable.", err=True)
58
63
  _exit(EXIT_INIT_FAILURE)
59
64
  try:
60
- engine = StataEngine(stata_path, ctx.obj["edition"], graphs_dir=ctx.obj.get("graphs_dir"))
65
+ engine = StataEngine(stata_path, ctx.obj["edition"], graphs_dir=ctx.obj.get("graphs_dir"), graph_format=ctx.obj.get("graph_format", "png"))
61
66
  return engine
62
67
  except Exception as exc:
63
68
  click.echo(f"Error initializing Stata: {exc}", err=True)
@@ -89,7 +94,25 @@ def _try_daemon(ctx, cmd_type: str, payload: dict) -> Result | None:
89
94
  return None
90
95
 
91
96
 
92
- def _print_result(result, compact: bool, use_json: bool = False, max_tokens: int = 0, filter_echo: bool = False) -> None:
97
+ def _try_daemon_dict(ctx, cmd_type: str, payload: dict) -> dict | None:
98
+ """Try to route a dict-returning command through daemon. Returns None if unavailable."""
99
+ if ctx.obj.get("no_daemon"):
100
+ return None
101
+ try:
102
+ from .daemon import DaemonClient
103
+ client = DaemonClient()
104
+ if not client.is_running():
105
+ return None
106
+ if not client.connect():
107
+ return None
108
+ resp = client.send(cmd_type, payload)
109
+ client.close()
110
+ return resp
111
+ except Exception:
112
+ return None
113
+
114
+
115
+ def _print_result(result, compact: bool, use_json: bool = False, max_tokens: int = 0, filter_echo: bool = False, log_file: str = None) -> None:
93
116
  output = result.output
94
117
  if output:
95
118
  output = clean_log_wrapper(output)
@@ -112,6 +135,13 @@ def _print_result(result, compact: bool, use_json: bool = False, max_tokens: int
112
135
  if graphs:
113
136
  for g in graphs:
114
137
  click.echo(f"[graph] {g.get('name', 'graph')}: {g.get('path', '')}")
138
+ if log_file and output:
139
+ try:
140
+ with open(log_file, "a", encoding="utf-8") as fh:
141
+ fh.write(output + "\n")
142
+ click.echo(f"[log] Output appended to: {log_file}")
143
+ except OSError as exc:
144
+ click.echo(f"[log] Failed to write log: {exc}", err=True)
115
145
  if not result.success:
116
146
  if result.error:
117
147
  click.echo(result.error, err=True)
@@ -144,7 +174,7 @@ def run(ctx, code):
144
174
  if result is None:
145
175
  engine = _get_engine(ctx)
146
176
  result = engine.run(code, timeout=ctx.obj["timeout"])
147
- _print_result(result, ctx.obj["compact"], use_json=ctx.obj["json"], max_tokens=ctx.obj["max_tokens"])
177
+ _print_result(result, ctx.obj["compact"], use_json=ctx.obj["json"], max_tokens=ctx.obj["max_tokens"], log_file=ctx.obj.get("log_file"))
148
178
 
149
179
 
150
180
  @cli.command("do")
@@ -162,7 +192,7 @@ def do_file(ctx, path):
162
192
  if result is None:
163
193
  engine = _get_engine(ctx)
164
194
  result = engine.run_file(path, timeout=ctx.obj["timeout"])
165
- _print_result(result, ctx.obj["compact"], use_json=ctx.obj["json"], max_tokens=ctx.obj["max_tokens"], filter_echo=True)
195
+ _print_result(result, ctx.obj["compact"], use_json=ctx.obj["json"], max_tokens=ctx.obj["max_tokens"], filter_echo=True, log_file=ctx.obj.get("log_file"))
166
196
 
167
197
 
168
198
  @cli.command()
@@ -248,6 +278,134 @@ def stop_cmd(ctx):
248
278
  _exit(EXIT_USAGE_ERROR)
249
279
 
250
280
 
281
+ @cli.command("return")
282
+ @click.argument("rtype", type=click.Choice(["r", "e", "s"], case_sensitive=False), default="r")
283
+ @click.pass_context
284
+ def return_cmd(ctx, rtype):
285
+ """Show stored Stata results (r/e/s).
286
+
287
+ \b
288
+ Examples:
289
+ stata-cli return r # r() results after a command
290
+ stata-cli return e # e() results after estimation
291
+ stata-cli return s # s() results
292
+ """
293
+ resp = _try_daemon_dict(ctx, "get_return", {"rtype": rtype.lower()})
294
+ if resp is None:
295
+ engine = _get_engine(ctx)
296
+ resp = engine.get_return(rtype.lower())
297
+ click.echo(json.dumps(resp, ensure_ascii=False, indent=2))
298
+
299
+
300
+ @cli.command("vars")
301
+ @click.argument("names", nargs=-1)
302
+ @click.pass_context
303
+ def vars_cmd(ctx, names):
304
+ """Show variable metadata for the current dataset.
305
+
306
+ \b
307
+ Examples:
308
+ stata-cli vars # all variables
309
+ stata-cli vars price mpg # specific variables
310
+ """
311
+ resp = _try_daemon_dict(ctx, "get_vars", {})
312
+ if resp is None:
313
+ engine = _get_engine(ctx)
314
+ resp = engine.get_vars()
315
+
316
+ if names and resp.get("status") == "success":
317
+ name_set = set(names)
318
+ resp["variables"] = [v for v in resp.get("variables", []) if v["name"] in name_set]
319
+ resp["n_vars"] = len(resp["variables"])
320
+
321
+ click.echo(json.dumps(resp, ensure_ascii=False, indent=2))
322
+
323
+
324
+ @cli.command("matrix")
325
+ @click.argument("name")
326
+ @click.pass_context
327
+ def matrix_cmd(ctx, name):
328
+ """Show a Stata matrix.
329
+
330
+ \b
331
+ Examples:
332
+ stata-cli matrix e(b) # coefficient vector
333
+ stata-cli matrix e(V) # variance-covariance matrix
334
+ stata-cli matrix r(table) # results table
335
+ """
336
+ resp = _try_daemon_dict(ctx, "get_matrix", {"name": name})
337
+ if resp is None:
338
+ engine = _get_engine(ctx)
339
+ resp = engine.get_matrix(name)
340
+ click.echo(json.dumps(resp, ensure_ascii=False, indent=2))
341
+
342
+
343
+ @cli.command("labels")
344
+ @click.argument("name", required=False, default=None)
345
+ @click.option("--var", default=None, help="Show value label attached to a variable.")
346
+ @click.pass_context
347
+ def labels_cmd(ctx, name, var):
348
+ """Show value labels.
349
+
350
+ \b
351
+ Examples:
352
+ stata-cli labels # list all value label names
353
+ stata-cli labels origin # show label-value mapping
354
+ stata-cli labels --var foreign # show label for a variable
355
+ """
356
+ resp = _try_daemon_dict(ctx, "get_labels", {"name": name, "var": var})
357
+ if resp is None:
358
+ engine = _get_engine(ctx)
359
+ resp = engine.get_labels(name=name, var=var)
360
+ click.echo(json.dumps(resp, ensure_ascii=False, indent=2))
361
+
362
+
363
+ @cli.command("macro")
364
+ @click.argument("action", type=click.Choice(["get", "set"], case_sensitive=False))
365
+ @click.argument("name")
366
+ @click.argument("value", required=False, default=None)
367
+ @click.pass_context
368
+ def macro_cmd(ctx, action, name, value):
369
+ """Get or set a Stata macro.
370
+
371
+ \b
372
+ Examples:
373
+ stata-cli macro get "c(current_date)"
374
+ stata-cli macro get "e(cmd)"
375
+ stata-cli macro set myvar "hello world"
376
+ """
377
+ if action.lower() == "set":
378
+ if value is None:
379
+ click.echo("Error: value is required for 'set'.", err=True)
380
+ _exit(EXIT_USAGE_ERROR)
381
+ resp = _try_daemon_dict(ctx, "set_macro", {"name": name, "value": value})
382
+ if resp is None:
383
+ engine = _get_engine(ctx)
384
+ resp = engine.set_macro(name, value)
385
+ else:
386
+ resp = _try_daemon_dict(ctx, "get_macro", {"name": name})
387
+ if resp is None:
388
+ engine = _get_engine(ctx)
389
+ resp = engine.get_macro(name)
390
+ click.echo(json.dumps(resp, ensure_ascii=False, indent=2))
391
+
392
+
393
+ @cli.command("frame")
394
+ @click.pass_context
395
+ def frame_cmd(ctx):
396
+ """List Stata frames and the current working frame.
397
+
398
+ \b
399
+ Examples:
400
+ stata-cli frame
401
+ """
402
+ resp = _try_daemon_dict(ctx, "get_frames", {})
403
+ if resp is None:
404
+ engine = _get_engine(ctx)
405
+ resp = engine.get_frames()
406
+ click.echo(json.dumps(resp, ensure_ascii=False, indent=2))
407
+
408
+
251
409
  # ── Daemon subcommands ───────────────────────────────────────────────────
252
410
 
253
411
  @cli.group()
@@ -1,4 +1,5 @@
1
1
  """Platform detection and Stata path auto-discovery."""
2
+ from __future__ import annotations
2
3
 
3
4
  import os
4
5
  import platform
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stata-cli
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Command-line interface for running Stata commands via PyStata
5
5
  License: MIT
6
6
  Keywords: stata,cli,statistics,data-science
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  Requires-Dist: click>=8.0
10
10
  Provides-Extra: data
@@ -13,8 +13,10 @@ Requires-Dist: pandas; extra == "data"
13
13
 
14
14
  # stata-cli
15
15
 
16
+ ![stata-cli banner](assets/banner.png)
17
+
16
18
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
- [![Python Version](https://img.shields.io/badge/python-%3E%3D3.10-blue.svg)](https://www.python.org/)
19
+ [![Python Version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://www.python.org/)
18
20
  [![npm version](https://img.shields.io/npm/v/stata-cli.svg)](https://www.npmjs.com/package/stata-cli)
19
21
 
20
22
  [中文版](README.zh.md) | [English](README.md)
@@ -39,10 +41,16 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
39
41
  | **Run Code** | Execute inline Stata code, multi-line blocks, or pipe from stdin |
40
42
  | **Do Files** | Run `.do` files with `///` line continuation support and graph auto-naming |
41
43
  | **Data Viewer** | View current dataset as JSON with `if`-condition filtering and row limits |
44
+ | **Variable Metadata** | Inspect variable names, types, formats, and labels via `vars` |
45
+ | **Stored Results** | Retrieve r(), e(), s() results as structured JSON via `return` |
46
+ | **Matrix Access** | Read Stata matrices (e.g. `e(b)`, `e(V)`) as JSON via `matrix` |
47
+ | **Value Labels** | List and inspect value labels via `labels` |
48
+ | **Macro Access** | Get/set Stata macros including `c()`, `e()`, `r()` system macros |
49
+ | **Frame Management** | List Stata frames and current working frame via `frame` |
42
50
  | **Help System** | Browse Stata help topics with SMCL-to-plain-text conversion |
43
- | **Graph Export** | Auto-detect and export graphs as PNG to `~/.stata-cli/graphs/` |
51
+ | **Graph Export** | Auto-detect and export graphs as PNG/SVG/PDF to `~/.stata-cli/graphs/` |
44
52
  | **Daemon Mode** | Persistent background process for sub-second execution via Unix socket |
45
- | **Output Control** | Compact mode, JSON output, token limit management with full-output file save |
53
+ | **Output Control** | Compact mode, JSON output, token limit management, log file output |
46
54
  | **Interruption** | Send break signal to stop long-running commands |
47
55
 
48
56
  ## Installation & Quick Start
@@ -50,7 +58,7 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
50
58
  ### Requirements
51
59
 
52
60
  - **Stata 17+** installed on your machine (provides the PyStata library)
53
- - Python 3.10+
61
+ - Python 3.9+
54
62
 
55
63
  ### Quick Start (Human Users)
56
64
 
@@ -197,6 +205,60 @@ stata-cli detect
197
205
 
198
206
  Prints the auto-detected Stata installation path.
199
207
 
208
+ ### `return` — Retrieve Stored Results
209
+
210
+ ```bash
211
+ stata-cli return r # r() results (after summarize, etc.)
212
+ stata-cli return e # e() results (after regress, etc.)
213
+ stata-cli return s # s() results
214
+ ```
215
+
216
+ Returns r(), e(), or s() stored results as structured JSON — scalars, macros, and matrix references.
217
+
218
+ ### `vars` — Variable Metadata
219
+
220
+ ```bash
221
+ stata-cli vars # all variables
222
+ stata-cli vars price mpg # specific variables
223
+ ```
224
+
225
+ Returns variable names, types, formats, and labels as JSON. More structured than `describe`.
226
+
227
+ ### `matrix` — Read Stata Matrices
228
+
229
+ ```bash
230
+ stata-cli matrix e(b) # coefficient vector
231
+ stata-cli matrix e(V) # variance-covariance matrix
232
+ ```
233
+
234
+ Returns matrix data, dimensions, and row/column names as JSON.
235
+
236
+ ### `labels` — Value Labels
237
+
238
+ ```bash
239
+ stata-cli labels # list all value label names
240
+ stata-cli labels origin # show value-label mapping
241
+ stata-cli labels --var foreign # show label attached to a variable
242
+ ```
243
+
244
+ ### `macro` — Get/Set Macros
245
+
246
+ ```bash
247
+ stata-cli macro get "c(current_date)"
248
+ stata-cli macro get "e(cmd)"
249
+ stata-cli macro set myvar "hello"
250
+ ```
251
+
252
+ Access Stata macros including system macros (`c()`, `e()`, `r()`).
253
+
254
+ ### `frame` — List Frames
255
+
256
+ ```bash
257
+ stata-cli frame
258
+ ```
259
+
260
+ Shows all Stata frames and the current working frame.
261
+
200
262
  ## Daemon Mode
201
263
 
202
264
  The daemon keeps PyStata alive in the background — reduces execution time from **~2-3s to ~85ms** (35x speedup).
@@ -234,6 +296,8 @@ The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle
234
296
  | `--max-tokens N` | Max output tokens (0=unlimited) | 0 |
235
297
  | `--no-daemon` | Force direct execution | off |
236
298
  | `--graphs-dir PATH` | Graph export directory | `~/.stata-cli/graphs/` |
299
+ | `--graph-format [png\|svg\|pdf]` | Graph export format | `png` |
300
+ | `--log PATH` | Save output to a log file | off |
237
301
 
238
302
  ### JSON Output
239
303
 
@@ -313,6 +377,21 @@ regress price mpg weight
313
377
  predict yhat
314
378
  list make price yhat in 1/5"
315
379
 
380
+ # Retrieve regression results as structured JSON
381
+ stata-cli return e
382
+
383
+ # Get coefficient matrix
384
+ stata-cli matrix e(b)
385
+
386
+ # Inspect variable metadata
387
+ stata-cli vars price mpg weight
388
+
389
+ # Check value labels
390
+ stata-cli labels --var foreign
391
+
392
+ # Read system macros
393
+ stata-cli macro get "c(N)"
394
+
316
395
  # Check data after loading
317
396
  stata-cli data --if "price>10000"
318
397
 
@@ -325,6 +404,9 @@ describe"
325
404
 
326
405
  # JSON mode for structured parsing
327
406
  stata-cli --json run "display 1+1"
407
+
408
+ # Export graph as SVG
409
+ stata-cli --graph-format svg run "scatter price mpg"
328
410
  ```
329
411
 
330
412
  ## Contributing
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
File without changes