File Coverage

blib/lib/Env/Dot/Functions.pm
Criterion Covered Total %
statement 124 127 97.6
branch 43 48 89.5
condition 13 17 76.4
subroutine 20 20 100.0
pod 5 5 100.0
total 205 217 94.4


line stmt bran cond sub pod time code
1             ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
2             package Env::Dot::Functions;
3 13     13   677735 use strict;
  13         29  
  13         489  
4 13     13   60 use warnings;
  13         19  
  13         739  
5              
6 13     13   59 use Cwd qw( abs_path );
  13         32  
  13         693  
7 13     13   907 use English qw( -no_match_vars );
  13         720  
  13         182  
8 13     13   3971 use File::Spec;
  13         24  
  13         327  
9 13     13   5004 use IO::File;
  13         72910  
  13         1405  
10              
11 13     13   119 use Exporter 'import';
  13         33  
  13         906  
12             our @EXPORT_OK = qw(
13             get_dotenv_vars
14             interpret_dotenv_filepath_var
15             get_envdot_filepaths_var_name
16             extract_error_msg
17             create_error_msg
18             );
19             our %EXPORT_TAGS = (
20             'all' => [
21             qw(
22             get_dotenv_vars
23             interpret_dotenv_filepath_var
24             get_envdot_filepaths_var_name
25             extract_error_msg
26             create_error_msg
27             )
28             ],
29             );
30              
31 13     13   65 use English qw( -no_match_vars ); # Avoids regex performance penalty in perl 5.18 and earlier
  13         17  
  13         76  
32 13     13   3212 use Carp;
  13         18  
  13         1141  
33              
34             # ABSTRACT: Read environment variables from .env file
35              
36             our $VERSION = '0.017_002'; # TRIAL VERSION: generated by DZP::OurPkgVersion
37              
38             use constant {
39 13         24994 OPTION_FILE_TYPE => q{file:type},
40             OPTION_FILE_TYPE_PLAIN => q{plain},
41             OPTION_FILE_TYPE_SHELL => q{shell},
42             DEFAULT_OPTION_FILE_TYPE => q{shell},
43             OPTION_READ_FROM_PARENT => q{read:from_parent},
44             DEFAULT_OPTION_READ_FROM_PARENT => 0,
45             OPTION_READ_ALLOW_MISSING_PARENT => q{read:allow_missing_parent},
46             DEFAULT_OPTION_READ_ALLOW_MISSING_PARENT => 0,
47 13     13   65 };
  13         15  
48              
49             my %DOTENV_OPTIONS = (
50             OPTION_READ_FROM_PARENT() => 1,
51             OPTION_READ_ALLOW_MISSING_PARENT() => 1,
52             'file:type' => 1,
53             'var:allow_interpolate' => 1,
54             );
55             my %DOS_PLATFORMS = (
56             'dos' => 'MS-DOS/PC-DOS',
57             'os2' => 'OS/2',
58             'MSWin32' => 'Windows',
59             'cygwin' => 'Cygwin',
60             );
61              
62             sub get_dotenv_vars {
63 14     14 1 30 my (@dotenv_filepaths) = @_;
64              
65 14         17 my @vars;
66 14         26 foreach my $filepath ( reverse @dotenv_filepaths ) {
67 22 50       406 if ( -f $filepath ) {
68 22         55 push @vars, _read_dotenv_file_recursively($filepath);
69             }
70             else {
71 0         0 my ($err) = "File not found: '$filepath'";
72 0         0 croak create_error_msg($err);
73             }
74             }
75 11         35 return @vars;
76             }
77              
78             sub interpret_dotenv_filepath_var {
79 21     21 1 215643 my ($var_content) = @_;
80 21 50       73 if ( exists $DOS_PLATFORMS{$OSNAME} ) {
81 0         0 return split qr{;}msx, $var_content;
82             }
83             else {
84 21         177 return split qr{:}msx, $var_content;
85             }
86             }
87              
88             sub get_envdot_filepaths_var_name {
89 20     20 1 78 return q{ENVDOT_FILEPATHS};
90             }
91              
92             # Private subroutines
93              
94             sub _read_dotenv_file_recursively {
95 27     27   19514 my ($filepath) = @_;
96 27         789 $filepath = abs_path($filepath);
97 27         69 my @rows = _read_dotenv_file($filepath);
98 27         64 my %r = _interpret_dotenv( $filepath, @rows );
99 25         34 my @these_vars = @{ $r{'vars'} };
  25         56  
100 25 100       58 if ( $r{'opts'}->{ OPTION_READ_FROM_PARENT() } ) {
101 7         18 my $parent_filepath = _get_parent_dotenv_filepath($filepath);
102 7 100       23 if ($parent_filepath) {
    100          
103 4         18 unshift @these_vars, _read_dotenv_file_recursively($parent_filepath);
104             }
105             elsif ( !$r{'opts'}->{ OPTION_READ_ALLOW_MISSING_PARENT() } ) {
106 2         4 my ($err) = "No parent .env file found for child file '$filepath'";
107 2         5 croak create_error_msg($err);
108             }
109             }
110 21         97 return @these_vars;
111             }
112              
113             # Follow directory hierarchy upwards until you find a .env file.
114             # If you don't, return undef.
115             # Otherwise return the path.
116             sub _get_parent_dotenv_filepath {
117 10     10   9985 my ($current_filepath) = @_;
118              
119 10         154 my ( $volume, $directories ) = File::Spec->splitpath($current_filepath);
120 10         70 my $parent_path = File::Spec->catpath( $volume, $directories );
121 10         15 my $parent_filepath;
122              
123 10   66     75 while ( defined $parent_path && $parent_path ne File::Spec->rootdir() ) {
124 22         626 $parent_path = abs_path( File::Spec->catdir( $parent_path, File::Spec->updir ) );
125 22         135 $parent_filepath = File::Spec->catfile( $parent_path, '.env' );
126 22 100 66     534 return $parent_filepath if ( defined $parent_path && -f $parent_filepath );
127             }
128 3         7 return;
129             }
130              
131             sub _interpret_dotenv {
132 34     34   23281 my ( $fp, @rows ) = @_;
133 34         153 my %options = (
134             OPTION_READ_FROM_PARENT() => DEFAULT_OPTION_READ_FROM_PARENT,
135             OPTION_READ_ALLOW_MISSING_PARENT() => DEFAULT_OPTION_READ_ALLOW_MISSING_PARENT,
136             'file:type' => DEFAULT_OPTION_FILE_TYPE,
137             'var:allow_interpolate' => 0,
138             ); # Options related to reading the file. Applied as they are read.
139 34         47 my @vars;
140 34         46 my $row_num = 1;
141 34         71 foreach (@rows) {
142             ## no critic (ControlStructures::ProhibitCascadingIfElse)
143             ## no critic (RegularExpressions::ProhibitComplexRegexes)
144 167 100       1100 if (
    100          
    100          
    100          
145             # This is envdot meta command
146             # The var: options can only apply to one subsequent var row.
147             m{
148             ^ [[:space:]]{0,} [#]{1}
149             [[:space:]]{1,} envdot [[:space:]]{1,}
150             [(] (? [^)]{0,}) [)]
151             [[:space:]]{0,} $
152             }msx
153             )
154             {
155 18         96 my $opts = _interpret_opts( $LAST_PAREN_MATCH{opts} );
156 18         68 foreach my $key ( keys %{$opts} ) {
  18         59  
157 26 100       63 if ( !exists $DOTENV_OPTIONS{$key} ) {
158 5         14 my $err = "Unknown envdot option: '$key'";
159 5         15 croak create_error_msg( $err, $row_num, $fp );
160             }
161             }
162 13         25 $options{'var:allow_interpolate'} = 0;
163 13         18 foreach ( keys %{$opts} ) {
  13         34  
164 21         46 $options{$_} = $opts->{$_};
165             }
166             }
167             elsif (
168             # This is comment row
169             m{
170             ^ [[:space:]]{0,} [#]{1} .* $
171             }msx
172             )
173             {
174 23         33 1;
175             }
176             elsif (
177             # This is empty row
178             m{
179             ^ [[:space:]]{0,} $
180             }msx
181             )
182             {
183 3         5 1;
184             }
185             elsif (
186             # This is env var description
187             m{
188             ^ (? [^=]{1,}) = (? .*) $
189             }msx
190             )
191             {
192 122         725 my ( $name, $value ) = ( $LAST_PAREN_MATCH{name}, $LAST_PAREN_MATCH{value} );
193 122 100       303 if ( $options{'file:type'} eq OPTION_FILE_TYPE_SHELL ) {
    50          
194 117 100       7835 if (
195             $value =~ m{
196             ^
197             ['"]{1} (? .*) ["']{1} # Get value from between quotes
198             (?: [;] [[:space:]]{0,} export [[:space:]]{1,} $name)? # optional
199             [[:space:]]{0,} # optional whitespace at the end
200             $
201             }msx
202             )
203             {
204 95         404 ($value) = $LAST_PAREN_MATCH{value};
205             }
206              
207             # "export" can also be at the start. Only for TYPE_SHELL
208 117 100       376 if ( $name =~ m{^ [[:space:]]{0,} export [[:space:]]{1,} }msx ) {
209 1         8 $name =~ m{
210             ^
211             [[:space:]]{0,} export [[:space:]]{1,} (? .*)
212             $
213             }msx;
214 1         8 $name = $LAST_PAREN_MATCH{name};
215             }
216             }
217             elsif ( $options{'file:type'} eq OPTION_FILE_TYPE_PLAIN ) {
218 5         9 1;
219             }
220 122         290 my %opts = ( allow_interpolate => $options{'var:allow_interpolate'}, );
221 122         370 push @vars, { name => $name, value => $value, opts => \%opts, };
222 122         209 $options{'var:allow_interpolate'} = 0;
223             }
224             else {
225 1         3 my $err = "Invalid line: '$_'";
226 1         4 croak create_error_msg( $err, $row_num, $fp );
227             }
228 161         248 $row_num++;
229             }
230 28         134 return opts => \%options, vars => \@vars;
231             }
232              
233             sub _interpret_opts {
234 24     24   265666 my ($opts_str) = @_;
235 24         284 my @opts = split qr{ [[:space:]]{0,} [,] [[:space:]]{0,} }msx, $opts_str;
236 24         66 my %opts;
237 24         51 foreach (@opts) {
238             ## no critic (ControlStructures::ProhibitPostfixControls)
239 37         200 my ( $key, $val ) = split qr/=/msx;
240 37   100     132 $val = $val // 1;
241 37 100 66     168 $val = 1 if ( $val eq 'true' || $val eq 'True' );
242 37 100 66     122 $val = 0 if ( $val eq 'false' || $val eq 'False' );
243 37         118 $opts{$key} = $val;
244             }
245 24         72 return \%opts;
246             }
247              
248             sub _read_dotenv_file {
249 27     27   54 my ($filepath) = @_;
250 27         153 my $fh = IO::File->new();
251 27         875 $fh->binmode(':encoding(UTF-8)');
252 27 50       272 $fh->open(qq{< $filepath}) or croak "Error: Cannot open file '$filepath'";
253 27         1746 my @dotenv_rows = <$fh>;
254 27         109 chomp @dotenv_rows;
255 27 50       118 $fh->close or croak "Error: Cannot close file '$filepath'";
256 27         505 return @dotenv_rows;
257             }
258              
259             # Error messages:
260             # Message structure:
261             # ! [line ] [file ]
262              
263             sub extract_error_msg {
264 9     9 1 178028 my ($msg) = @_;
265 9 100       29 if ( !$msg ) {
266 1         191 croak 'Parameter error: missing parameter \'msg\'';
267             }
268             ## no critic (RegularExpressions::ProhibitComplexRegexes)
269 8         73 my ( $err, $line, $filepath ) =
270             $msg =~ m/^ ([^!]{1,}) \! (?: \s line \s ([[:digit:]]{1,}) (?: \s file \s \'([^']{1,})\' )? )? .* $/msx;
271 8         33 return $err, $line, $filepath;
272             }
273              
274             sub create_error_msg {
275 15     15 1 7986 my ( $err, $line, $filepath ) = @_;
276 15 100       73 if ( !$err ) {
277 1         135 croak 'Parameter error: missing parameter \'err\'';
278             }
279 14 100 100     58 if ( !$line && $filepath ) {
280 1         232 croak 'Parameter error: missing parameter \'line\'';
281             }
282 13 100       1448 return "${err}!" . ( defined $line ? " line ${line}" : q{} ) . ( defined $filepath ? " file '${filepath}'" : q{} );
    100          
283             }
284              
285             1;
286              
287             __END__