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 }