Line data Source code
1 : // Copyright (c) 2015-2022 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // This program is free software; you can redistribute it and/or modify
4 : // it under the terms of the GNU General Public License as published by
5 : // the Free Software Foundation; either version 2 of the License, or
6 : // (at your option) any later version.
7 : //
8 : // This program is distributed in the hope that it will be useful,
9 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 : // GNU General Public License for more details.
12 : //
13 : // You should have received a copy of the GNU General Public License along
14 : // with this program; if not, write to the Free Software Foundation, Inc.,
15 : // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 :
17 : /** \file
18 : * \brief Implementation of the CSS Preprocessor command line tool.
19 : * \tableofcontents
20 : *
21 : * This tool can be used as a verification, compilation, and compression
22 : * tool depending on your needs.
23 : *
24 : * The Snap! Websites environment uses the tool for verification when
25 : * generating a layout. Later a Snap! Website plugin compresses the various
26 : * files. That way the website system includes the original file and not
27 : * just the minimized version.
28 : *
29 : * \section csspp-options Command Line Options
30 : *
31 : * The following are the options currently supported by csspp:
32 : *
33 : * \subsection arguments --args or -a -- specifying arguments
34 : *
35 : * The SCSS scripts expect some variables to be set. Some of these variables
36 : * can be set on the command line with the --args option. The arguments are
37 : * added to an array that can be accessed as the variable $_csspp_args.
38 : *
39 : * \code
40 : * // command line
41 : * csspp --args red -- my-file.scss
42 : *
43 : * // reference to the command line argument
44 : * .flowers
45 : * {
46 : * border: 1px solid rgb(identifier($_csspp_args[1]));
47 : * }
48 : * \endcode
49 : *
50 : * \warning
51 : * This example does not work yet because I did not yet implement the
52 : * rgb() internal function to transform input in a COLOR token. However,
53 : * I intend to work on the colors soonish and thus it could be fully
54 : * functional by the time you read the example.
55 : *
56 : * At this time there is no other way to access command line arguments.
57 : *
58 : * There is no $_csspp_args[0] since arrays in SCSS start at 1. This
59 : * also means you do not (yet) have access to the name of the program
60 : * compiling the code.
61 : *
62 : * Multiple arguments can be specified one after another:
63 : *
64 : * \code
65 : * csspp --args red green blue -- my-file.css
66 : * \endcode
67 : *
68 : * \subsection debug --debug or -d -- show all messages, including @debug messages
69 : *
70 : * When specified, the error output is setup to output everything,
71 : * including fatal errors, errors, warnings, informational messages,
72 : * and debug messages.
73 : *
74 : * \subsection help --help or -h -- show the available command line options
75 : *
76 : * The --help command line option can be used to request that the csspp
77 : * print out the complete list of supported command line options in
78 : * stdout.
79 : *
80 : * The tool then quits immediately.
81 : *
82 : * \subsection include -I -- specify paths to include files
83 : *
84 : * Specify paths to user defined directories that include SCSS scripts
85 : * one can include using the @import command.
86 : *
87 : * By default the system looks for system defined scripts (i.e. the
88 : * default validation, version, and other similar scripts) under
89 : * the following directory:
90 : *
91 : * \code
92 : * /usr/lib/csspp/scripts
93 : * \endcode
94 : *
95 : * The system scripts (initialization, closure, version) appear under
96 : * a sub-directory named "system".
97 : *
98 : * The validation scripts (field names, pseudo names, etc.) appear
99 : * under a sub-directory named "validation".
100 : *
101 : * There are no specific rules for where include files will be found.
102 : * The @import can use a full path or a local path. When a local path
103 : * is used, then all the specified -I paths are prepended until a
104 : * file matches. The first match is used.
105 : *
106 : * You may specify any number of include paths one after another. You
107 : * must specify -I only once:
108 : *
109 : * \code
110 : * csspp ... -I my-scripts alfred-scripts extension-scripts ...
111 : * \endcode
112 : *
113 : * \subsection no_logo --no-logo -- hide the "logo"
114 : *
115 : * This option prevents the "logo" comment from being added at the end
116 : * of the output.
117 : *
118 : * \subsection output --output or -o -- specify the output
119 : *
120 : * This option may be used to specify a filename used to save the
121 : * output of the compiler. By default the output is written to
122 : * stdout.
123 : *
124 : * You may explicitly use '-' to write the output to stdout.
125 : *
126 : * \code
127 : * csspp --output file.css my-script.scss
128 : * \endcode
129 : *
130 : * \subsection precision --precision or -p -- specify the precision to use with decimal number
131 : *
132 : * The output is written as consice as possible. Only that can cause problems
133 : * with decimal numbers getting written with less precision than you need.
134 : *
135 : * By default decimal numbers are written with 3 decimal numbers after the
136 : * decimal point. You may use the --precision command line option to change
137 : * that default to another value.
138 : *
139 : * \code
140 : * csspp ... --precision 5 ...
141 : * \endcode
142 : *
143 : * Note that numbers such as 3.5 are not written with ending zeroes (i.e.
144 : * 3.50000) even if you increase precision.
145 : *
146 : * \warning
147 : * The percent numbers, which are also decimal numbers, do not take this
148 : * value in account. All percent numbers are always written with 2 decimal
149 : * digits after the decimal point. We may change that behavior in the
150 : * future if someone sees a need for it.
151 : *
152 : * \subsection quiet --quiet or -q -- make the output as quite as possible
153 : *
154 : * By default csspp prints out all messages except debug messages.
155 : *
156 : * This option also turns off informational and warning messages. So in
157 : * effect all that's left are error and fatal error messages.
158 : *
159 : * Note that if you used the --Werror command line options, warning
160 : * are transformed to errors and thus they get printed in your output
161 : * anyway.
162 : *
163 : * \subsection style --style or -s -- define the output style
164 : *
165 : * By default the csspp compiler is expected to compress your CSS data
166 : * as much as possible (i.e. it removes non-required spaces, delete empty
167 : * rules, avoid new lines, etc.)
168 : *
169 : * The --style options let choose a different output style than the
170 : * compressed style:
171 : *
172 : * \li --style compressed -- this is the default, it outputs files as
173 : * compressed as possible
174 : * \li --style tidy -- this option writes one rule per line, each rule is
175 : * as compressed as possible
176 : * \li --compact -- this option writes one declaration per line, making it
177 : * a lot easier to edit if you were to do such a thing; this output is
178 : * already quite gentle on humans and can easily be used for debug purposes
179 : * \li expanded -- this option prints everything as neatly as possible
180 : * for human consumption; the output uses many newlines and indentation
181 : * for declarations
182 : *
183 : * The best to see how each style really looks like is for you to test
184 : * with a large existing CSS file and check the output of csspp against
185 : * that file.
186 : *
187 : * For example, you could use the \c expanded format before reading a
188 : * file you found on a website as in:
189 : *
190 : * \code
191 : * csspp --style expanded compressed.css
192 : * \endcode
193 : *
194 : * \subsection version --version -- print out the version and exit
195 : *
196 : * This command line option prints out the version of the csspp compiler
197 : * in stdout and then exits.
198 : *
199 : * \subsection warnings-to-errors --Werror -- transform warnings into errors
200 : *
201 : * The --Werror requests the compiler to generate errors whenever
202 : * a warning message was to be printed. This also has the side effect
203 : * of incrementing the error counter by one each time a warning is
204 : * found. Note that as a result the warning counter will always
205 : * remains zero nin this case.
206 : *
207 : * \note
208 : * You may want to note that this option uses two dashes (--) to specify.
209 : * With GNU C/C++, the command line accepts -Werror, with a single dash.
210 : *
211 : * \subsection command-line-filenames Input files
212 : *
213 : * Other parameters specified on the command line, or parameters defined
214 : * after a "--", are taken as .scss filenames. The "--" is mandatory if
215 : * you have a preceeding argument that accepts multiple values like the
216 : * --args and -I options.
217 : *
218 : * \code
219 : * // no need for "--" in this case:
220 : * csspp -I scripts -p 2 my-script.scss
221 : *
222 : * // "--" required in this case:
223 : * csspp -p 2 -I scripts -- my-script.scss
224 : * \endcode
225 : */
226 :
227 : // csspp
228 : //
229 : #include <csspp/assembler.h>
230 : #include <csspp/compiler.h>
231 : #include <csspp/exception.h>
232 : #include <csspp/parser.h>
233 :
234 :
235 : // advgetopt
236 : //
237 : #include <advgetopt/advgetopt.h>
238 : #include <advgetopt/exception.h>
239 :
240 :
241 : // boost
242 : //
243 : #include <boost/preprocessor/stringize.hpp>
244 :
245 :
246 : // C++
247 : //
248 : #include <cstdlib>
249 : #include <fstream>
250 : #include <iostream>
251 :
252 :
253 : // C
254 : //
255 : #include <unistd.h>
256 :
257 :
258 : // last include
259 : //
260 : #include <snapdev/poison.h>
261 :
262 :
263 :
264 : namespace
265 : {
266 :
267 1 : void free_char(char * ptr)
268 : {
269 1 : free(ptr);
270 1 : }
271 :
272 : // TODO: add support for configuration files & the variable
273 :
274 : constexpr advgetopt::option g_options[] =
275 : {
276 : advgetopt::define_option(
277 : advgetopt::Name("args")
278 : , advgetopt::ShortName('a')
279 : , advgetopt::Flags(advgetopt::command_flags<
280 : advgetopt::GETOPT_FLAG_REQUIRED
281 : , advgetopt::GETOPT_FLAG_MULTIPLE>())
282 : , nullptr
283 : , "define values in the $_csspp_args variable map"
284 : , nullptr
285 : ),
286 : advgetopt::define_option(
287 : advgetopt::Name("debug")
288 : , advgetopt::ShortName('d')
289 : , advgetopt::Flags(advgetopt::standalone_command_flags<>())
290 : , advgetopt::Help("show all messages, including @debug messages")
291 : ),
292 : advgetopt::define_option(
293 : advgetopt::Name("include")
294 : , advgetopt::ShortName('I')
295 : , advgetopt::Flags(advgetopt::command_flags<
296 : advgetopt::GETOPT_FLAG_REQUIRED
297 : , advgetopt::GETOPT_FLAG_MULTIPLE>())
298 : , advgetopt::Help("specify one or more paths to various user defined CSS files; \"-\" to clear the list (i.e. \"-I -\")")
299 : ),
300 : advgetopt::define_option(
301 : advgetopt::Name("no-logo")
302 : , advgetopt::ShortName('\0')
303 : , advgetopt::Flags(advgetopt::standalone_command_flags<>())
304 : , advgetopt::Help("prevent the \"logo\" from appearing in the output file")
305 : ),
306 : advgetopt::define_option(
307 : advgetopt::Name("empty-on-undefined-variable")
308 : , advgetopt::ShortName('\0')
309 : , advgetopt::Flags(advgetopt::standalone_command_flags<>())
310 : , advgetopt::Help("if accessing an undefined variable, return an empty string, otherwise generate an error.")
311 : ),
312 : advgetopt::define_option(
313 : advgetopt::Name("output")
314 : , advgetopt::ShortName('o')
315 : , advgetopt::Flags(advgetopt::command_flags<
316 : advgetopt::GETOPT_FLAG_REQUIRED>())
317 : , advgetopt::Help("save the results in the specified file if specified; otherwise send output to stdout.")
318 : ),
319 : advgetopt::define_option(
320 : advgetopt::Name("precision")
321 : , advgetopt::ShortName('p')
322 : , advgetopt::Flags(advgetopt::command_flags<
323 : advgetopt::GETOPT_FLAG_REQUIRED>())
324 : , advgetopt::Help("define the number of digits to use after the decimal point, defaults to 3; note that for percent values, the precision is always 2.")
325 : ),
326 : advgetopt::define_option(
327 : advgetopt::Name("quiet")
328 : , advgetopt::ShortName('q')
329 : , advgetopt::Flags(advgetopt::standalone_command_flags<>())
330 : , advgetopt::Help("suppress @info and @warning messages.")
331 : ),
332 : advgetopt::define_option(
333 : advgetopt::Name("style")
334 : , advgetopt::ShortName('s')
335 : , advgetopt::Flags(advgetopt::command_flags<
336 : advgetopt::GETOPT_FLAG_REQUIRED>())
337 : , advgetopt::Help("output style: compressed, tidy, compact, expanded.")
338 : ),
339 : advgetopt::define_option(
340 : advgetopt::Name("Werror")
341 : , advgetopt::Flags(advgetopt::standalone_command_flags<>())
342 : , advgetopt::Help("make warnings count as errors.")
343 : ),
344 : advgetopt::define_option(
345 : advgetopt::Name("--")
346 : , advgetopt::Flags(advgetopt::command_flags<
347 : advgetopt::GETOPT_FLAG_MULTIPLE
348 : , advgetopt::GETOPT_FLAG_DEFAULT_OPTION
349 : , advgetopt::GETOPT_FLAG_SHOW_USAGE_ON_ERROR>())
350 : , advgetopt::Help("[file.css ...]; use stdin if no filename specified.")
351 : ),
352 : advgetopt::end_options()
353 : };
354 :
355 : // TODO: once we have stdc++20, remove all defaults
356 : #pragma GCC diagnostic ignored "-Wpedantic"
357 : advgetopt::options_environment const g_options_environment =
358 : {
359 : .f_project_name = "csspp",
360 : .f_group_name = nullptr,
361 : .f_options = g_options,
362 : .f_options_files_directory = nullptr,
363 : .f_environment_variable_name = "CSSPPFLAGS",
364 : .f_environment_variable_intro = nullptr,
365 : .f_section_variables_name = nullptr,
366 : .f_configuration_files = nullptr,
367 : .f_configuration_filename = nullptr,
368 : .f_configuration_directories = nullptr,
369 : .f_environment_flags = advgetopt::GETOPT_ENVIRONMENT_FLAG_PROCESS_SYSTEM_PARAMETERS,
370 : .f_help_header = "Usage: %p [-<opt>] [file.css ...] [-o out.css]\n"
371 : "where -<opt> is one or more of:",
372 : .f_help_footer = "%c",
373 : .f_version = CSSPP_VERSION,
374 : .f_license = "GNU GPL v2",
375 : .f_copyright = "Copyright (c) 2015-"
376 : BOOST_PP_STRINGIZE(UTC_BUILD_YEAR)
377 : " by Made to Order Software Corporation -- All Rights Reserved",
378 : //.f_build_date = UTC_BUILD_DATE,
379 : //.f_build_time = UTC_BUILD_TIME
380 : };
381 :
382 :
383 :
384 : class pp
385 : {
386 : public:
387 : pp(int argc, char * argv[]);
388 :
389 : int compile();
390 :
391 : private:
392 : advgetopt::getopt f_opt;
393 : int f_precision = 3;
394 : };
395 :
396 :
397 1 : pp::pp(int argc, char * argv[])
398 1 : : f_opt(g_options_environment, argc, argv)
399 : {
400 1 : if(f_opt.is_defined("quiet"))
401 : {
402 0 : csspp::error::instance().set_hide_all(true);
403 : }
404 :
405 1 : if(f_opt.is_defined("debug"))
406 : {
407 1 : csspp::error::instance().set_show_debug(true);
408 : }
409 :
410 1 : if(f_opt.is_defined("Werror"))
411 : {
412 0 : csspp::error::instance().set_count_warnings_as_errors(true);
413 : }
414 :
415 1 : if(f_opt.is_defined("precision"))
416 : {
417 0 : f_precision = f_opt.get_long("precision");
418 : }
419 1 : }
420 :
421 1 : int pp::compile()
422 : {
423 1 : csspp::lexer::pointer_t l;
424 1 : csspp::position::pointer_t pos;
425 1 : std::unique_ptr<std::stringstream> ss;
426 :
427 1 : csspp::safe_precision_t safe_precision(f_precision);
428 :
429 1 : if(f_opt.is_defined("--"))
430 : {
431 : // one or more filename specified
432 1 : int const arg_count(f_opt.size("--"));
433 2 : if(arg_count == 1
434 2 : && f_opt.get_string("--") == "-")
435 : {
436 : // user asked for stdin
437 0 : pos.reset(new csspp::position("-"));
438 0 : l.reset(new csspp::lexer(std::cin, *pos));
439 : }
440 : else
441 : {
442 1 : std::unique_ptr<char, void (*)(char *)> cwd(get_current_dir_name(), free_char);
443 1 : ss.reset(new std::stringstream);
444 1 : pos.reset(new csspp::position("csspp.css"));
445 2 : for(int idx(0); idx < arg_count; ++idx)
446 : {
447 : // full paths so the -I have no effects on those files
448 3 : std::string filename(f_opt.get_string("--", idx));
449 1 : if(filename.empty())
450 : {
451 0 : csspp::error::instance() << *pos
452 0 : << "You cannot include a file with an empty name."
453 0 : << csspp::error_mode_t::ERROR_WARNING;
454 0 : return 1;
455 : }
456 1 : if(filename == "-")
457 : {
458 0 : csspp::error::instance() << *pos
459 0 : << "You cannot currently mix files and stdin. You may use @import \"filename\"; in your stdin data though."
460 0 : << csspp::error_mode_t::ERROR_WARNING;
461 0 : return 1;
462 : }
463 1 : if(filename[0] == '/')
464 : {
465 : // already absolute
466 1 : *ss << "@import \"" << filename << "\";\n";
467 : }
468 : else
469 : {
470 : // make absolute so we do not need to have a "." path
471 0 : *ss << "@import \"" << cwd.get() << "/" << filename << "\";\n";
472 : }
473 1 : }
474 1 : l.reset(new csspp::lexer(*ss, *pos));
475 1 : }
476 : }
477 : else
478 : {
479 : // default to stdin
480 0 : pos.reset(new csspp::position("-"));
481 0 : l.reset(new csspp::lexer(std::cin, *pos));
482 : }
483 :
484 : // run the lexer and parser
485 1 : csspp::error_happened_t error_tracker;
486 2 : csspp::parser p(l);
487 1 : csspp::node::pointer_t root(p.stylesheet());
488 1 : if(error_tracker.error_happened())
489 : {
490 0 : return 1;
491 : }
492 :
493 1 : csspp::node::pointer_t csspp_args(new csspp::node(csspp::node_type_t::LIST, root->get_position()));
494 1 : csspp::node::pointer_t args_var(new csspp::node(csspp::node_type_t::VARIABLE, root->get_position()));
495 1 : args_var->set_string("_csspp_args");
496 1 : csspp::node::pointer_t wrapper(new csspp::node(csspp::node_type_t::LIST, root->get_position()));
497 1 : csspp::node::pointer_t array(new csspp::node(csspp::node_type_t::ARRAY, root->get_position()));
498 1 : wrapper->add_child(array);
499 1 : csspp_args->add_child(args_var);
500 1 : csspp_args->add_child(wrapper);
501 1 : if(f_opt.is_defined("args"))
502 : {
503 0 : int const count(f_opt.size("args"));
504 0 : for(int idx(0); idx < count; ++idx)
505 : {
506 0 : csspp::node::pointer_t arg(new csspp::node(csspp::node_type_t::STRING, root->get_position()));
507 0 : arg->set_string(f_opt.get_string("args", idx));
508 0 : array->add_child(arg);
509 0 : }
510 : }
511 1 : root->set_variable("_csspp_args", csspp_args);
512 :
513 : // run the compiler
514 1 : csspp::compiler c;
515 1 : c.set_root(root);
516 1 : c.set_date_time_variables(time(nullptr));
517 :
518 : // add paths to the compiler (i.e. for the user and system @imports)
519 1 : if(f_opt.is_defined("include"))
520 : {
521 1 : std::size_t const count(f_opt.size("include"));
522 3 : for(std::size_t idx(0); idx < count; ++idx)
523 : {
524 6 : std::string const path(f_opt.get_string("include", idx));
525 2 : if(path == "-")
526 : {
527 0 : c.clear_paths();
528 : }
529 : else
530 : {
531 2 : c.add_path(path);
532 : }
533 2 : }
534 : }
535 :
536 1 : if(f_opt.is_defined("no-logo"))
537 : {
538 0 : c.set_no_logo();
539 : }
540 :
541 1 : if(f_opt.is_defined("empty-on-undefined-variable"))
542 : {
543 0 : c.set_empty_on_undefined_variable(true);
544 : }
545 :
546 1 : c.compile(false);
547 1 : if(error_tracker.error_happened())
548 : {
549 0 : return 1;
550 : }
551 :
552 1 : csspp::output_mode_t output_mode(csspp::output_mode_t::COMPRESSED);
553 1 : if(f_opt.is_defined("style"))
554 : {
555 3 : std::string const mode(f_opt.get_string("style"));
556 1 : if(mode == "compressed")
557 : {
558 0 : output_mode = csspp::output_mode_t::COMPRESSED;
559 : }
560 1 : else if(mode == "tidy")
561 : {
562 0 : output_mode = csspp::output_mode_t::TIDY;
563 : }
564 1 : else if(mode == "compact")
565 : {
566 0 : output_mode = csspp::output_mode_t::COMPACT;
567 : }
568 1 : else if(mode == "expanded")
569 : {
570 1 : output_mode = csspp::output_mode_t::EXPANDED;
571 : }
572 : else
573 : {
574 0 : csspp::error::instance() << root->get_position()
575 0 : << "The output mode \""
576 0 : << mode
577 0 : << "\" is not supported. Try one of: compressed, tidy, compact, expanded instead."
578 0 : << csspp::error_mode_t::ERROR_WARNING;
579 0 : return 1;
580 : }
581 1 : }
582 :
583 1 : std::ostream * out(nullptr);
584 1 : bool user_output(false);
585 1 : std::string output_filename;
586 1 : if(f_opt.is_defined("output"))
587 : {
588 1 : output_filename = f_opt.get_string("output");
589 1 : user_output = output_filename != "-";
590 : }
591 1 : if(user_output)
592 : {
593 1 : out = new std::ofstream(output_filename);
594 : }
595 : else
596 : {
597 0 : out = &std::cout;
598 : }
599 1 : csspp::assembler a(*out);
600 1 : a.output(c.get_root(), output_mode);
601 1 : if(user_output)
602 : {
603 1 : delete out;
604 : }
605 1 : if(error_tracker.error_happened())
606 : {
607 : // this should be rare as the assembler generally does not generate
608 : // errors (it may throw though.)
609 0 : return 1;
610 : }
611 :
612 1 : return 0;
613 1 : }
614 :
615 : } // no name namespace
616 :
617 1 : int main(int argc, char *argv[])
618 : {
619 : try
620 : {
621 1 : pp preprocessor(argc, argv);
622 1 : return preprocessor.compile();
623 1 : }
624 0 : catch(advgetopt::getopt_exit const & except)
625 : {
626 0 : return except.code();
627 0 : }
628 0 : catch(csspp::csspp_exception_exit const & e)
629 : {
630 : // something went wrong in the library
631 0 : return e.exit_code();
632 0 : }
633 0 : catch(csspp::csspp_exception_logic const & e)
634 : {
635 0 : std::cerr << "fatal error: a logic exception, which should NEVER occur, occurred: " << e.what() << std::endl;
636 0 : exit(1);
637 0 : }
638 0 : catch(csspp::csspp_exception_overflow const & e)
639 : {
640 0 : std::cerr << "fatal error: an overflow exception occurred: " << e.what() << std::endl;
641 0 : exit(1);
642 0 : }
643 0 : catch(csspp::csspp_exception_runtime const & e)
644 : {
645 0 : std::cerr << "fatal error: a runtime exception occurred: " << e.what() << std::endl;
646 0 : exit(1);
647 0 : }
648 0 : catch(advgetopt::getopt_undefined const & e)
649 : {
650 0 : std::cerr << "fatal error: an undefined exception occurred because of your command line: " << e.what() << std::endl;
651 0 : exit(1);
652 0 : }
653 0 : catch(advgetopt::getopt_invalid const & e)
654 : {
655 0 : std::cerr << "fatal error: there is an error on your command line, an exception occurred: " << e.what() << std::endl;
656 0 : exit(1);
657 0 : }
658 0 : catch(advgetopt::getopt_invalid_default const & e)
659 : {
660 0 : std::cerr << "fatal error: there is an error on your command line, you used a parameter without a value and there is no default. The exception says: " << e.what() << std::endl;
661 0 : exit(1);
662 0 : }
663 : }
664 :
665 : // Local Variables:
666 : // mode: cpp
667 : // indent-tabs-mode: nil
668 : // c-basic-offset: 4
669 : // tab-width: 4
670 : // End:
671 :
672 : // vim: ts=4 sw=4 et
|