-
Notifications
You must be signed in to change notification settings - Fork 2
/
cli.lisp
193 lines (169 loc) · 7.08 KB
/
cli.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
(in-package :cl-user)
(uiop:define-package :rosa/cli
(:use :cl)
(:import-from :rosa
:index
:peruse-as-plist
:pick)
(:export :dump-all
:index-labels
:option-command
:parse-argv
:pick-bodies
:print-usage))
(in-package :rosa/cli)
(defun print-usage ()
(format *error-output* "Rosa - text labeling language CLI
usage: rosa index [OPTIONS] [FILE]
rosa pick [OPTIONS] LABEL [FILE]
rosa dump [OPTIONS] [FILE]
DESCRIPTION
Rosa extract key-value structure from input. Key as 'label' and value as 'body'.
If not supplied FILE, rosa reads from standard input.
COMMANDS
Each commands have its shorthand; the first letter. For instance, shorthand of
`index` is `i`.
rosa index [FILE]
List all labels from input. By default, formatted as line-based plain text.
rosa pick LABEL [FILE]
Pick up the value(s) corresponding to the label, from input. By default,
rosa returns only first body of all appearance.
rosa dump [FILE]
Parse and print entire key-value data from input.
FORMATTING OPTIONS
Output formatting type. By default, output formatted as S-expression.
-s
Format output as S-expression.
-j
Format output as JSON.
-y
Format output as YAML.
PICKING UP OPTIONS
Available with pick command.
-a
If supplied, all bodies are printed as formatting type.
-n NUM
Specify number of bodies picking up from head. By default, no supplied,
NUM set as 1.
-t
Tell rosa to pick up tail.
"))
(defstruct option
command format-type pick-label pick-all pick-nth pick-tail pathname)
(defparameter +commands+
'((:index "i" "index")
(:pick "p" "pick")
(:dump "d" "dump")))
(defun commandp (s)
(find s +commands+
:test #'(lambda (i s) (member i s :test #'string=))))
(defun error-and-exit (s)
(format t "~a~%~%" s)
(uiop:quit 1))
(defun parse-argv (argv)
(let ((option (make-option)))
(flet ((optionp (a)
(char= (aref a 0) #\-))
(find-format-type (a)
(setf (option-format-type option)
(cond ((string= a "-s") :sexp)
((string= a "-j") :json)
((string= a "-y") :yaml)))))
(let* ((s (first argv))
(command (first (commandp s))))
(if (or (not s) (not command))
(error-and-exit (format nil "unknown command: ~s" s))
(setf (option-command option) command)))
(loop
:for n :from 1 :below (length argv)
:with command := (option-command option)
:for a string := (nth n argv)
:do (if (optionp a)
(cond ((member a '("-s" "-j" "-y") :test #'string=)
(when (not (option-format-type option))
(find-format-type a)))
((and (eq command :pick)
(string= a "-a"))
(setf (option-pick-all option) t))
((and (eq command :pick)
(string= a "-t"))
(setf (option-pick-tail option) t))
((and (eq command :pick)
(string= a "-n"))
(incf n)
(handler-case
(setf (option-pick-nth option) (parse-integer (nth n argv)))
(condition (c)
(declare (ignore c))
(error-and-exit (format nil "~s is not positive integer"
(nth n argv))))))
(t (error-and-exit (format nil "unknown option: ~s" a))))
(cond ((and (eq command :pick)
(not (option-pick-label option)))
(setf (option-pick-label option) (intern a :keyword)))
(t (if (not (option-pathname option))
(setf (option-pathname option) (pathname a))
(error-and-exit "too many arguments"))))))
option)))
(defmacro with-input ((var pathname) &body body)
`(if ,pathname
(if (probe-file ,pathname)
(with-open-file (,var ,pathname
:direction :input
:external-format (inq:make-external-format
:utf-8
(inq:detect-end-of-line (pathname ,pathname))))
,@body)
(error-and-exit (format nil "no such file: ~s" ,pathname)))
(let ((,var *standard-input*))
,@body)))
(defun index-labels (option)
(with-input (in (option-pathname option))
(let ((labels (rosa:index in)))
(case (option-format-type option)
(:sexp (format t "~s~%" labels))
(:json (format t "~a~%"
(jojo:to-json (loop
:for l :in labels
:collect (symbol-name l)))))
(:yaml (format t "~a~%"
(yaml:emit-to-string
(loop
:for l :in labels
:collect (symbol-name l)))))
(t (loop :for l :in labels :do (format t "~a~%" l)))))))
(defun pick-bodies (option)
(with-input (in (option-pathname option))
(flet ((print-body (body format-type)
(case format-type
(:sexp (format t "~s~%" body))
(:json (format t "~a~%" (jojo:to-json body)))
(:yaml (format t "~a~%" (yaml:emit-to-string body)))
(t (format t "~a~%" body))))
(expected-body (bodies)
(let ((pick-nth (option-pick-nth option)))
(handler-case
(cond ((option-pick-all option) bodies)
((option-pick-tail option) (aref bodies (1- (length bodies))))
(pick-nth (aref bodies pick-nth))
(t (aref bodies 0)))
(condition (c)
(error-and-exit (format nil "~a" c)))))))
(let* ((expected (expected-body (rosa:pick in (option-pick-label option)))))
(if (stringp expected)
(print-body expected nil)
(print-body expected
(or (option-format-type option) :sexp)))))))
(defun dump-all (option)
(with-input (in (option-pathname option))
(let ((format-type (or (option-format-type option) :sexp)))
(flet ((string-keyed-data ()
(let ((newhash (make-hash-table :test #'equal)))
(maphash (lambda (k v)
(setf (gethash (symbol-name k) newhash) v))
(rosa:peruse in))
newhash)))
(case format-type
(:sexp (format t "~s~%" (rosa:peruse-as-plist in)))
(:json (format t "~a~%" (jojo:to-json (rosa:peruse-as-plist in))))
(:yaml (format t "~a~%" (yaml:emit-to-string (string-keyed-data)))))))))