The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!/usr/bin/perl
####################################################################################################
# sass (scss) compiler
####################################################################################################
use utf8;
use strict;
####################################################################################################
# dependencies
####################################################################################################
# parse options
# make the options case sensitive
Getopt::Long::Configure("bundling");
# load constants from libsass
use CSS::Sass qw(SASS_STYLE_EXPANDED);
use CSS::Sass qw(SASS_STYLE_NESTED);
use CSS::Sass qw(SASS_STYLE_COMPRESSED);
use CSS::Sass qw(SASS_STYLE_COMPACT);
use CSS::Sass::Watchdog qw(start_watchdog);
use CSS::Sass::Plugins qw(%plugins);
# setup encodings for std streams
# not sure why this is not default
binmode(STDIN, ":encoding(console_in)");
binmode(STDOUT, ":encoding(console_out)");
binmode(STDERR, ":encoding(console_out)");
####################################################################################################
# normalize command arguments to utf8
####################################################################################################
# get cmd arg encoding
use Encode::Locale qw();
# convert cmd args to utf8
use Encode qw(decode encode);
# now just decode every command arguments
@ARGV = map { decode(locale => $_, 1) } @ARGV;
####################################################################################################
# options variables
####################################################################################################
# init options
my $watchdog;
my $benchmark;
my $precision;
my $output_file;
my $output_style;
my $source_comments;
my $source_map_file;
my $source_map_root;
my $source_map_embed;
my $source_map_contents;
my $source_map_file_urls;
my $omit_source_map_url;
# paths arrays
my @plugin_paths;
my @include_paths;
# output styles
my $indent = " ";
my $linefeed = "auto";
####################################################################################################
####################################################################################################
# define a sub to print out the version (mimic behaviour of node.js blessc)
# this script has it's own version numbering as it's not dependent on any libs
sub version {
printf "psass %s (perl sass/scss compiler)\n", "0.5.0";
printf " libsass: %s\n", CSS::Sass::libsass_version();
printf " sass2scss: %s\n", CSS::Sass::sass2scss_version();
exit 0 };
sub list_plugins {
foreach my $plugin (sort keys %plugins) {
printf "--%s-plugin\n", $plugin;
}
exit 0 };
####################################################################################################
####################################################################################################
my %options = (
'help|h' => sub { pod2usage(1); },
'watch|w!' => \ $watchdog,
'version|v' => \ &version,
'benchmark|b!' => \ $benchmark,
'indent=s' => \ $indent,
'linefeed=s' => \ $linefeed,
'precision|p=s' => \ $precision,
'output-file|o=s' => \ $output_file,
'output-style|t=s' => \ $output_style,
'line-numbers|c!' => \ $source_comments,
'line-comments|c!' => \ $source_comments,
'source-comments|c!' => \ $source_comments,
'source-map-file|m=s' => \ $source_map_file,
'source-map-root=s' => \ $source_map_root,
'source-map-embed|e!' => \ $source_map_embed,
'source-map-contents|s!' => \ $source_map_contents,
'source-map-file-urls!' => \$source_map_file_urls,
'no-source-map-url!' => \ $omit_source_map_url,
'plugin-path|P|L=s' => sub { push @plugin_paths, $_[1] },
'include-path|I=s' => sub { push @include_paths, $_[1] }
);
my %enable_plugins;
# add shortcuts for known plugins
foreach my $plugin (keys %plugins) {
$enable_plugins{$plugin} = 0;
$options{"${plugin}-plugin!"} =
\ $enable_plugins{$plugin};
}
$options{'list-plugins'} = sub { list_plugins };
$options{'all-plugins'} = sub {
foreach my $plugin (keys %plugins) {
$enable_plugins{$plugin} = 1;
}
};
# get options
GetOptions %options;
# register the enabled plugin paths
foreach my $plugin (keys %enable_plugins) {
next unless $enable_plugins{$plugin};
push @plugin_paths, $plugins{$plugin};
}
# set default if not configured
unless (defined $output_style)
{ $output_style = SASS_STYLE_NESTED }
# parse string to constant
elsif ($output_style =~ m/^n/i)
{ $output_style = SASS_STYLE_NESTED }
elsif ($output_style =~ m/^compa/i)
{ $output_style = SASS_STYLE_COMPACT }
elsif ($output_style =~ m/^compr/i)
{ $output_style = SASS_STYLE_COMPRESSED }
elsif ($output_style =~ m/^e/i)
{ $output_style = SASS_STYLE_EXPANDED }
# die with message if style is unknown
else { die "unknown output style: $output_style" }
# resolve linefeed options
if ($linefeed =~ m/^a/i)
{ $linefeed = undef; }
elsif ($linefeed =~ m/^w/i)
{ $linefeed = "\r\n"; }
elsif ($linefeed =~ m/^[u]/i)
{ $linefeed = "\n"; }
elsif ($linefeed =~ m/^[n]/i)
{ $linefeed = ""; }
# die with message if linefeed type is unknown
else { die "unknown linefeed type: $linefeed" }
# do we have output path in second arg?
if (defined $ARGV[1] && $ARGV[1] ne '-')
{ $output_file = $ARGV[1]; }
# check if the benchmark module is available
if ($benchmark && ! eval "use Benchmark; 1" )
{ die "Error loading Benchmark module\n", $@; }
####################################################################################################
# get sass standard option list
####################################################################################################
sub sass_options ()
{
return (
dont_die => $watchdog,
indent => $indent,
linefeed => $linefeed,
precision => $precision,
output_path => $output_file,
output_style => $output_style,
plugin_paths => \ @plugin_paths,
include_paths => \ @include_paths,
source_comments => $source_comments,
source_map_file => $source_map_file,
source_map_root => $source_map_root,
source_map_embed => $source_map_embed,
source_map_contents => $source_map_contents,
source_map_file_urls => $source_map_file_urls,
omit_source_map_url => $omit_source_map_url,
);
}
####################################################################################################
# implement our own safe File::Slurp::write_file
# since syswrite() is deprecated on :utf8 handles
####################################################################################################
# To mark FILEHANDLE as UTF-8, use :utf8 or :encoding(UTF-8) . :utf8 just marks the
# data as UTF-8 without further checking, while :encoding(UTF-8) checks the data for
# actually being valid UTF-8. More details can be found in PerlIO::encoding.
sub write_file ($$)
{
use Fcntl qw(:flock LOCK_EX);
# my ($file, $content, $binmode) = @_;
# avoid arg copies and use @_ directly
if (open (my $fh, ">", $_[0])) {
unless (flock($fh, LOCK_EX)) {
Carp::croak "Error aquiring file lock!\n",
"Path: ", $_[0], "\n",
"Reason: ", $!, "\n";
}
unless (binmode($fh, $_[2] || ':utf8')) {
Carp::croak "Error setting file mode!\n",
"Path: ", $_[0], "\n",
"Reason: ", $!, "\n";
}
unless (print $fh $_[1]) {
Carp::croak "Error writing output!\n",
"Path: ", $_[0], "\n",
"Reason: ", $!, "\n";
}
}
else {
Carp::croak "Error opening writable file!\n",
"Path: ", $_[0], "\n",
"Reason: ", $!, "\n";
}
}
####################################################################################################
use CSS::Sass qw(sass_compile_file sass_compile);
####################################################################################################
# first run we always want to die on error
# because we will not get any included files
our $error = sub { die @_ };
sub compile ()
{
# variables
my ($css, $err, $stats);
# get benchmark stamp before compiling
my $t0 = $benchmark ? Benchmark->new : 0;
# open filehandle if path is given
if (defined $ARGV[0] && $ARGV[0] ne '-')
{
($css, $err, $stats) = sass_compile_file(
$ARGV[0], sass_options()
);
}
# or use standard input
else
{
($css, $err, $stats) = sass_compile(
join('', <STDIN>), sass_options()
);
}
# get benchmark stamp after compiling
my $t1 = $benchmark ? Benchmark->new : 0;
# only print benchmark result when module is available
if ($benchmark) { warn timestr(timediff($t1, $t0), 'auto', '5.4f'), "\n"; }
# process return status values
if (defined $css)
{
# by default we just print to standard out
unless (defined $output_file) { print $css; }
# or if output_file is defined via options we write it there
else { write_file($output_file, $css ); }
}
elsif (defined $err) { $error->($err); }
else { $error->("fatal error - aborting"); }
# output source-map
if ($source_map_file)
{
my $smap = $stats->{'source_map_string'};
unless ($smap) { $error->("source-map not generated <$source_map_file>") }
else { write_file($source_map_file, $smap ); }
}
# return according to expected return type
return wantarray ? ($css, $err, $stats) : $css;
}
####################################################################################################
# main program execution
####################################################################################################
my ($css, $err, $stats) = compile();
if ($watchdog)
{
local $error = sub { warn @_ };
start_watchdog($stats, \&compile);
}
####################################################################################################
####################################################################################################
__END__
=head1 NAME
psass - perl sass (scss) compiler
=head1 SYNOPSIS
psass [options] [ path_in | - ] [ path_out | - ]
Options:
-v, --version print version
-h, --help print this help
-w, --watch start watchdog mode
-p, --precision=int precision for float output
--indent=string set indent string used for output
--linefeed=type linefeed used for output [auto|unix|win|none]
-o, --output-file=file output file to write result to
-t, --output-style=style output style [expanded|nested|compressed|compact]
-P, --plugin-path=path plugin load path (repeatable)
-I, --include-path=path sass include path (repeatable)
-c, --source-comments enable source debug comments
-l, --line-comments synonym for --source-comments
--line-numbers synonym for --source-comments
-e, --source-map-embed embed source-map in mapping url
-s, --source-map-contents include original contents
-m, --source-map-file=file create and write source-map to file
--source-map-file-urls create file urls for source paths
--source-map-root=. specific root for relative paths
--no-source-map-url omit sourceMappingUrl from output
--benchmark print benchmark for compilation time
Plugins may be pre-installed by CSS::Sass or from 3rd parties.
There are some options available for each known plugin.
--all-plugins enables all known plugins
--list-plugin print list of all known plugins
--[name]-plugin enables the plugin with [name]
--no-[name]-plugin disabled the plugin with [name]
=head1 OPTIONS
=over 8
=item B<-help>
Print a brief help message with options and exits.
=back
=head1 DESCRIPTION
B<This program> is a sass (scss) compiler
=cut