Something went wrong. Try again.
A CLI to (readonly) query Scrivener project in textual form
scrivener cli
Something went wrong. Try again.
7.3 kB · 224 lines
Python
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225"""``scrivcli`` command-line interface (read-only Scrivener project viewer)."""
from __future__ import annotations
import argparseimport sysfrom pathlib import Pathfrom typing import Never
from scriv import __version__from scriv.project import ProjectError, ResolveError, ScrivenerProjectfrom scriv.render import ( display_title, render_flat, render_meta, render_tree,)
def build_parser() -> argparse.ArgumentParser: formatter = argparse.RawDescriptionHelpFormatter parser = argparse.ArgumentParser( prog="scrivcli", description="""Read a Scrivener project without modifying or locking it.
Supported projects: - Scrivener 2 and 3 for macOS - Scrivener 1.x for Windows
Command output is written to stdout; errors are written to stderr.""", epilog="""document references: Commands accepting <doc> support an ID/UUID, an exact title (case-insensitive), or a slash-path printed by `ls --flat`.
exit status: 0 success 1 project, document, or I/O error 2 invalid command-line usage
Run `scrivcli <command> --help` for command-specific examples.""", formatter_class=formatter, ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) sub = parser.add_subparsers(dest="command", required=True, metavar="<command>")
p_ls = sub.add_parser( "ls", help="list the binder (document tree)", description="""List every binder item in the project.
By default, output is a tree with metadata badges. Flat output usesslash-paths that can be passed to `show` and `meta`.""", epilog="""examples: scrivcli ls Novel.scriv scrivcli ls Novel.scriv --ids scrivcli ls Novel.scriv --flat --text-only --no-badges""", formatter_class=formatter, ) p_ls.add_argument("project", help="path to a .scriv bundle") p_ls.add_argument("--flat", action="store_true", help="print slash-paths instead of a tree") p_ls.add_argument("--ids", action="store_true", help="include document IDs/UUIDs") p_ls.add_argument("--no-badges", action="store_true", help="hide metadata badges") p_ls.add_argument( "--text-only", action="store_true", help="list only text documents (requires --flat)" ) p_ls.set_defaults(func=cmd_ls)
p_show = sub.add_parser( "show", help="print a document's text", description="""Print the body of one text document.
RTF is converted to plain text unless --raw is given. Synopsis andmetadata are not included; use `meta` to inspect them.""", epilog="""examples: scrivcli show Novel.scriv "Draft/Chapter 1/Waking Up" scrivcli show Novel.scriv 01234567-89AB-CDEF-0123-456789ABCDEF scrivcli show Novel.scriv "Draft/Chapter 1" --raw""", formatter_class=formatter, ) p_show.add_argument("project", help="path to a .scriv bundle") p_show.add_argument("doc", help="ID/UUID, exact title, slash-path, or (Untitled)") p_show.add_argument( "--raw", action="store_true", help="write the stored RTF bytes without conversion" ) p_show.set_defaults(func=cmd_show)
p_meta = sub.add_parser( "meta", help="show a document's metadata", description="""Print metadata for one binder item.
Output includes its title, type, ID, path, dates, label, status,keywords, compile setting, and icon. Text documents also include theirword count and synopsis.""", epilog="""examples: scrivcli meta Novel.scriv "Draft/Chapter 1/Waking Up" scrivcli meta Novel.scriv 01234567-89AB-CDEF-0123-456789ABCDEF""", formatter_class=formatter, ) p_meta.add_argument("project", help="path to a .scriv bundle") p_meta.add_argument("doc", help="ID/UUID, exact title, slash-path, or (Untitled)") p_meta.set_defaults(func=cmd_meta)
p_info = sub.add_parser( "info", help="show project overview", description="""Print an overview of the project.
The document count includes text documents throughout the binder. Theword count is the sum of their converted plain-text word counts.""", epilog="""example: scrivcli info Novel.scriv""", formatter_class=formatter, ) p_info.add_argument("project", help="path to a .scriv bundle") p_info.set_defaults(func=cmd_info)
return parser
def open_project(path: str) -> ScrivenerProject: try: return ScrivenerProject(Path(path)) except ProjectError as exc: _die(str(exc))
def cmd_ls(args: argparse.Namespace) -> int: project = open_project(args.project) if args.flat: out = render_flat( project, ids=args.ids, show_badges=not args.no_badges, text_only=args.text_only, ) else: out = render_tree(project, ids=args.ids, show_badges=not args.no_badges) _print(out) return 0
def cmd_show(args: argparse.Namespace) -> int: project = open_project(args.project) item = _resolve(project, args.doc) if item is None: _die(f"Could not resolve {args.doc!r}.") if not item.is_text: _die(f"{display_title(item)!r} is a {item.type}, not a text document.") if args.raw: _write_bytes(project.raw_bytes(item)) else: _print(project.document_text(item)) return 0
def cmd_meta(args: argparse.Namespace) -> int: project = open_project(args.project) item = _resolve(project, args.doc) _print(render_meta(project, item)) return 0
def cmd_info(args: argparse.Namespace) -> int: project = open_project(args.project) docs = project.documents() total_words = sum(project.word_count(d) for d in docs) info = project.info lines = [ f"Project {project.bundle}", f"Identifier {info.identifier}", f"Version {info.version} (creator {info.creator})", f"Author {info.author or '-'}", f"Device {info.device or '-'}", f"Modified {info.modified or '-'}", f"Format {project.format.value}", f"Documents {len(docs)}", f"Words {total_words}", ] _print("\n".join(lines)) return 0
def _resolve(project: ScrivenerProject, ref: str): try: return project.resolve(ref) except ResolveError as exc: if exc.candidates: _print(f"Multiple documents match {ref!r}:", file=sys.stderr) for c in exc.candidates: _print(f" - {c.slash_path()} <id {c.id}>", file=sys.stderr) _die("Refine the reference (use a slash-path or ID).") _die(str(exc))
def _print(msg: str, *, file=None) -> None: print(msg, file=file) # noqa: T201
def _write_bytes(data: bytes) -> None: sys.stdout.flush() sys.stdout.buffer.write(data) sys.stdout.buffer.flush()
def _die(msg: str) -> Never: print(f"scrivcli: {msg}", file=sys.stderr) raise SystemExit(1)
def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) if args.command == "ls" and args.text_only and not args.flat: parser.error("ls: --text-only requires --flat") try: return args.func(args) except (OSError, ProjectError) as exc: _die(str(exc))
if __name__ == "__main__": raise SystemExit(main())