ovedpo15 has asked for the wisdom of the Perl Monks concerning the following question:

Hi Monks!
Under a UNIX system, I want to create a sub which gets a path and resolves any env in the path. My current code is:
sub resolve_env_in_path { my ($path) = @_; $path =~ s/\$\{(\w+)\}/$ENV{$1}/g; $path =~ s/\$(\w+)/$ENV{$1}/g; return $path; }
It works ok but I have two problems I'm trying to solve:
1. I don't resolve multi envs in the path.
2. I don't check if $ENV{$1} is defined. Actually there could be paths like /a/b/c/file$ or /a/b/c/fi$le where $ is just inside the path (no such env).
To solve the first problem, I could do a while loop and try to replace every single env, until there is no envs left. But how do I treat the second problem?

Replies are listed 'Best First'.
Re: How to replace envs in path?
by BillKSmith (Monsignor) on May 09, 2022 at 15:06 UTC
    It is usually easier to treat errors and special cases with Perl rather than a regex. In this case, do one field at a time. I do not know the error processing that you need, but this should get you started.
    use strict; use warnings; use Test::More tests=>2; my $raw_path = '$HOME/work_dir/${TOOL_NAME}/$VERSION'; my $required_path = 'home/work_dir/hammer/1.01'; my %env = ( # Dummy hash for testing HOME => 'home', TOOL_NAME => 'hammer', #VERSION => '1.01', # Removed to force error ); sub resolve_env_in_path { my ($path) = @_; while ($path =~ m/\$\{?(\w+)\}?/) { my $field; if( exists($env{$1}) ) { $field = $env{$1}; } else { warn "Invallid path"; $field = 'UNSPECIFIED'; } $path =~ s/\$\{?(\w+)\}?/$field/; } return $path; }; ok( resolve_env_in_path($raw_path) ne $required_path, 'error expected' + ); $env{VERSION} = '1.01'; ok( resolve_env_in_path($raw_path) eq $required_path, 'good' );
    Bill
      Hi Bill, thank you!
      Based on your answer, I built the following sub:
      sub resolve_envs { my ($path) = @_; while ($path =~ m/[^\\]\$\{?(\w+)\}?/) { my $env = $1; if (exists($ENV{$env})) { $path =~ s/([^\\])\$\{?(\w+)\}?/$1$ENV{$env}/; } else { $path =~ s/([^\\])\$\{?(\w+)\}?/$1\\\$$env/; } } $path =~ s/\\\$/\$/g; return $path; }
      It works good for most of the cases. The only corner case that it does not work is that if you have a file that already contains "\$" (not just "$", already escaped). For example:
      set playground="${HOME}/playground" set file1=${playground}'/fi$le1' set file2=${playground}'/fi\$le2' mkdir -p ${playground} touch ${file1} touch ${file2}
      For the first one it works, but for the second one it does not because I remove the extra backslashes that I added so you get ${playground}'/fi$le2. Actually it won't work for any escaped chars. Any ideas how can I handle this corner case?
        I really do not understand your test cases. Please post code that we can run and duplicate both your successes and your failures (and know the difference). Note how my example used Test::More to show that my function did exactly what I expected (Perhaps not what you wanted, but you can tell). You have a good start with your pass and fail test cases. Unfortunately, we cannot tell if they are single or doubly quoted strings or if you intend to include newlines. The same thing applies to your expected results. Are backslashes literal or are they escapes? The result fragments you posted are much harder to test than complete strings.
        Bill
Re: How to replace envs in path?
by Anonymous Monk on May 09, 2022 at 14:28 UTC

    To take care of undefined environment variables you can code something like

    $path =~ s<\$(\w+}/>< $ENV{$1} // $1 >ge;
    

    if you are under at least Perl 5.10. The /e qualifier causes the replacement to be interpreted as an expression, not a string. Note that I had to change the delimiters to something other than C<'/'> to accommodate the defined-or operator.

Re: How to replace envs in path?
by Don Coyote (Hermit) on May 09, 2022 at 11:58 UTC
    happy half way to midweek ovedpo15,

    Are you rolling your own shell script parser, or sending in strings of symbolic scalar refs from another part of your perl script?

      Hi!
      Thanks for the replay. I get a custom list of paths and I create a structure which represents the filesystem. I'm trying to support environment variables inside the list of paths so hence the question. Just want to support paths like $HOME/work_dir/${TOOL_NAME}/$VERSION.

        the paths you do no want to expand are any where the scalar sigil $ is not immediately after the filepath separator. In which case you can allow these by matching for that construct.

        my $fps = 'filepathseparator'; m! $fps # your file path separator \Q$\E # quoted scalar sigil (shell variable expansion) \{? # start shelling out for a return value ? ([^\W]+) # your environment variable, not containing, # one or more non-word characters \}? # end shelling out for a return value ? !x

        This should match both cases where the curly braces are or are not used while constraining the variable to starting immediately after the file_path_separator (no mid/end word sigils match), and not allowing the path word to contain any non word characters.