1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module dsrcgen.sh;
7 
8 import std.typecons : Yes, Flag;
9 
10 public import dsrcgen.base;
11 
12 version (Have_unit_threaded) {
13     import unit_threaded : shouldEqual;
14 } else {
15     /// Fallback when unit_threaded doon't exist.
16     private void shouldEqual(T0, T1)(T0 value, T1 expect) {
17         assert(value == expect, value);
18     }
19 }
20 
21 @safe:
22 
23 /** A sh comment using '#' as is.
24  *
25  * Compared to Text a comment is affected by indentation.
26  */
27 class Comment : BaseModule {
28     mixin Attrs;
29 
30     private string contents;
31 
32     /// Construct a one-liner comment from contents.
33     this(string contents) {
34         this.contents = contents;
35     }
36 
37     override string renderIndent(int parent_level, int level) {
38         if ("begin" in attrs) {
39             return indent(attrs["begin"] ~ contents, parent_level, level);
40         }
41 
42         return indent("# " ~ contents, parent_level, level);
43     }
44 }
45 
46 /** A sh statement.
47  *
48  * Affected by attribute end.
49  * stmt ~ end
50  *    <recursive>
51  */
52 class Stmt(T) : T {
53     private string headline;
54 
55     ///
56     this(string headline) {
57         this.headline = headline;
58     }
59 
60     /// See_Also: BaseModule
61     override string renderIndent(int parent_level, int level) {
62         auto end = "end" in attrs;
63         string r = headline ~ (end is null ? "" : *end);
64 
65         if ("noindent" !in attrs) {
66             r = indent(r, parent_level, level);
67         }
68 
69         return r;
70     }
71 }
72 
73 /** A shell block wrapped by default in '{}'.
74  *
75  * Affected by attribute begin, end, noindent.
76  * headline ~ begin
77  *     <recursive>
78  * end
79  * noindent affects post_recursive. If set no indention there.
80  * r.length > 0 catches the case when begin or end is empty string. Used in switch/case.
81  */
82 class Suite(T) : T {
83     private string headline;
84 
85     ///
86     this(string headline) {
87         this.headline = headline;
88     }
89 
90     override string renderIndent(int parent_level, int level) {
91         import std.ascii : newline;
92 
93         string r;
94         if (auto begin = "begin" in attrs) {
95             r = headline ~ *begin;
96         } else {
97             r = headline ~ " {" ~ newline;
98         }
99 
100         if (r.length > 0 && "noindent" !in attrs) {
101             r = indent(r, parent_level, level);
102         }
103         return r;
104     }
105 
106     override string renderPostRecursive(int parent_level, int level) {
107         string r = "}";
108         if (auto end = "end" in attrs) {
109             r = *end;
110         }
111 
112         if (r.length > 0 && "noindent" !in attrs) {
113             r = indent(r, parent_level, level);
114         }
115         return r;
116     }
117 }
118 
119 class ShModule : BaseModule {
120     mixin Attrs;
121 
122     /** Access to self.
123      *
124      * Useful in with-statements.
125      */
126     auto _() {
127         return this;
128     }
129 
130     /** An empty node holdig other nodes.
131      *
132      * Not affected by indentation.
133      */
134     auto empty() {
135         auto e = new Empty!(typeof(this));
136         append(e);
137         return e;
138     }
139 
140     /** Make a Comment followed by a separator.
141      *
142      * Affected by indentation.
143      *
144      * TODO should have an addSep like stmt have.
145      */
146     Comment comment(string comment) {
147         auto e = new Comment(comment);
148         e.sep;
149         append(e);
150         return e;
151     }
152 
153     /** Make a raw Text.
154      *
155      * Note it is intentional that the text object do NOT have a separator. It
156      * is to allow detailed "surgical" insertion of raw text/data when no
157      * semantical "helpers" exist for a specific use case.
158      */
159     auto text(string content) pure {
160         auto e = new Text!(typeof(this))(content);
161         append(e);
162         return e;
163     }
164 
165     /** A basic building block with no content.
166      *
167      * Useful when a "node" is needed to add further content in.
168      * The node is affected by indentation.
169      */
170     auto base() {
171         auto e = new typeof(this);
172         append(e);
173         return e;
174     }
175 
176     /** Make a statement with an optional separator.
177      *
178      * A statement is commonly an individual item or at the most a line.
179      *
180      * Params:
181      *   stmt_ = raw text to use as the statement
182      *   separator = flag determining if a separator is added
183      *
184      * Returns: Stmt instance stored in this.
185      */
186     auto stmt(string stmt_, Flag!"addSep" separator = Yes.addSep) {
187         auto e = new Stmt!(typeof(this))(stmt_);
188         append(e);
189         if (separator) {
190             sep();
191         }
192         return e;
193     }
194 
195     /** Make a suite/block as a child of "this" with an optional separator.
196      *
197      * The separator is inserted after the block.
198      *
199      * Returns: Suite instance stored in this.
200      */
201     auto suite(string headline, Flag!"addSep" separator = Yes.addSep) {
202         auto e = new Suite!(typeof(this))(headline);
203         append(e);
204         if (separator) {
205             sep();
206         }
207         return e;
208     }
209 
210     // === Statements ===
211     auto shebang(string s) {
212         auto e = text("#!" ~ s);
213         sep();
214         return e;
215     }
216 
217     // === Suites ===
218 }
219 
220 /** Generate a shell script with shebang
221  */
222 struct ShScriptModule {
223     /// Shell root.
224     ShModule doc;
225     /// Shebang at the top.
226     ShModule shebang;
227     /// Shell content
228     ShModule content;
229 
230     /// Make a sh-script
231     static auto make() {
232         ShScriptModule m;
233         m.doc = new ShModule;
234         m.doc.suppressIndent(1);
235 
236         m.shebang = m.doc.base;
237         m.shebang.suppressIndent(1);
238 
239         m.content = m.doc.base;
240         m.content.suppressIndent(1);
241 
242         return m;
243     }
244 
245     auto render() {
246         return doc.render();
247     }
248 }
249 
250 @("Shall be a comment")
251 unittest {
252     auto m = new ShModule;
253     m.comment("a comment");
254 
255     m.render.shouldEqual("    # a comment
256 ");
257 }
258 
259 @("Shall be a sh statement")
260 unittest {
261     auto m = new ShModule;
262     m.stmt("echo");
263 
264     m.render.shouldEqual("    echo
265 ");
266 }
267 
268 @("Shall be a sh block")
269 unittest {
270     auto m = new ShModule;
271 
272     with (m.suite("for")) {
273         stmt("echo");
274     }
275 
276     m.render.shouldEqual("    for {
277         echo
278     }
279 ");
280 }
281 
282 @("Shall be a shell script")
283 unittest {
284     auto sh = ShScriptModule.make();
285 
286     sh.shebang.shebang("/bin/sh");
287     sh.content.stmt("echo");
288 
289     sh.render.shouldEqual("#!/bin/sh
290 echo
291 ");
292 }