EnvExpander.cpp
Go to the documentation of this file.
1/**
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * 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
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @author Fabian Reister ( fabian dot reister at kit dot edu )
17 * @date 2025
18 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
19 * GNU General Public License
20 */
21
22#include "EnvExpander.h"
23
24#include <cstdlib>
25#include <iostream>
26#include <stdexcept>
27#include <string>
28#include <tuple>
29#include <unordered_set>
30#include <vector>
31
32#include <boost/algorithm/string/classification.hpp>
33#include <boost/algorithm/string/split.hpp>
34#include <boost/algorithm/string/trim.hpp>
35#include <boost/process/environment.hpp>
36#include <boost/regex.hpp> // IWYU pragma: keep
37
38// these are the individual includes for boost::regex v5
39// #include <boost/regex/v5/regex.hpp>
40// #include <boost/regex/v5/regex_fwd.hpp>
41// #include <boost/regex/v5/regex_replace.hpp>
42
43#include <SimoxUtility/algorithm/string/string_tools.h>
44
45namespace
46{
47 // Expand leading `~` in each colon-separated segment to $HOME. This is
48 // file-local to keep expandVariables shorter and clearer.
49 void
50 expandTildeSegments(std::string& text)
51 {
52 if (text.empty())
53 {
54 return;
55 }
56
57 const char* home = std::getenv("HOME");
58 bool warnedHome = false;
59
60 std::vector<std::string> segments;
61 boost::split(segments, text, boost::is_any_of(":"));
62 for (auto& seg : segments)
63 {
64 if (!seg.empty() && seg[0] == '~')
65 {
66 // Only treat `~` or `~/...` as expansion
67 if (seg.size() == 1 || seg[1] == '/')
68 {
69 if (home == nullptr)
70 {
71 if (!warnedHome)
72 {
73 std::cerr << "[EnvExpander] Warning: HOME is not defined, cannot "
74 "expand '~'\n";
75 warnedHome = true;
76 }
77 seg.erase(0, 1); // remove leading '~'
78 }
79 else
80 {
81 std::string h(home);
82 // replace leading '~' with home
83 seg.erase(0, 1);
84 seg.insert(0, h);
85 }
86 }
87 }
88 }
89
90 // Rejoin
91 std::string joined;
92 for (size_t i = 0; i < segments.size(); ++i)
93 {
94 if (i != 0)
95 {
96 joined += ':';
97 }
98 joined += segments[i];
99 }
100 text = joined;
101 }
102
103} // anonymous namespace
104
106{
107
108
109 EnvExpander::EnvExpander() : env_(boost::this_process::environment())
110 {
111 }
112
113 void
114 EnvExpander::apply(const std::string& input, bool expandExisting)
115 {
116 // check if we need to expand existing env vars. It could be that there is nothing to do
117 if (not simox::alg::contains(input, "="))
118 {
119 return;
120 }
121
122 if (expandExisting)
123 {
124 // version that takes the updated env into account
125
126 auto assignments = splitAssignments(input);
127
128 for (const auto& assign : assignments)
129 {
130 auto [name, op, value] = splitOperation(assign);
131 auto expanded = expandVariables(value);
132 applyOperation(name, op, expanded);
133 }
134 }
135 else
136 {
137 // version that does not take the updated env into account
138
139 std::string expanded = expand(input);
140 auto assignments = splitAssignments(expanded);
141 for (const auto& assign : assignments)
142 {
143 auto [name, op, value] = splitOperation(assign);
144 applyOperation(name, op, value);
145 }
146 }
147 }
148
149 std::string
150 EnvExpander::expand(const std::string& input)
151 {
152 // check if we need to expand existing env vars. It could be that there is nothing to do
153 if (not simox::alg::contains(input, "="))
154 {
155 return input;
156 }
157
158 auto assignments = splitAssignments(input);
159
160 std::vector<std::string> out;
161 out.reserve(assignments.size());
162
163 for (const auto& assign : assignments)
164 {
165 auto [name, op, value] = splitOperation(assign);
166 auto expanded = expandVariables(value);
167
168 std::string rep;
169 switch (op)
170 {
172 {
173 rep = name;
174 rep += '=';
175 rep += expanded;
176 break;
177 }
179 {
180 // Read current env value and expand its variables as well
181 const char* existing = std::getenv(name.c_str());
182 std::string oldVal = (existing != nullptr) ? existing : std::string();
183 std::string oldExpanded = expandVariables(oldVal);
184
185 rep = name;
186 rep += '=';
187 if (!oldExpanded.empty())
188 {
189 rep += oldExpanded;
190 rep += ':';
191 }
192 rep += expanded;
193 break;
194 }
196 {
197 const char* existing = std::getenv(name.c_str());
198 std::string oldVal = (existing != nullptr) ? existing : std::string();
199 std::string oldExpanded = expandVariables(oldVal);
200
201 rep = name;
202 rep += '=';
203 rep += expanded;
204 if (!oldExpanded.empty())
205 {
206 rep += ':';
207 rep += oldExpanded;
208 }
209 break;
210 }
211 }
212
213 out.push_back(rep);
214 }
215
216 // Join with commas
217 std::string joined;
218 for (size_t i = 0; i < out.size(); ++i)
219 {
220 if (i != 0)
221 {
222 joined += ",";
223 }
224 joined += out[i];
225 }
226
227 return joined;
228 }
229
230 std::vector<std::string>
231 EnvExpander::splitAssignments(const std::string& input)
232 {
233 std::vector<std::string> out;
234 boost::split(out, input, boost::is_any_of(","));
235 return out;
236 }
237
238 std::tuple<std::string, EnvExpander::Operation, std::string>
239 EnvExpander::splitOperation(const std::string& assignment)
240 {
241 auto pos = assignment.find('=');
242 if (pos == std::string::npos)
243 {
244 throw std::runtime_error("Invalid assignment: " + assignment);
245 }
246
247 std::string key = assignment.substr(0, pos);
248 std::string rhs = assignment.substr(pos + 1);
249
250 boost::trim(key);
251 boost::trim(rhs);
252
253 // Detect prepend: VAR=+value
254 if (!rhs.empty() && rhs[0] == '+')
255 {
256 return {key, Operation::Prepend, rhs.substr(1)};
257 }
258
259 // Detect append: VAR+=value
260 if (!key.empty() && key.back() == '+')
261 {
262 key.pop_back();
263 return {key, Operation::Append, rhs};
264 }
265
266 // Replace (default)
267 return {key, Operation::Replace, rhs};
268 }
269
270 std::string
271 EnvExpander::expandVariables(const std::string& value)
272 {
273 std::string result = value;
274
275 // Expand leading tilde segments to HOME (if present)
276 expandTildeSegments(result);
277
278 static const boost::regex reBraced(R"(\$\{([A-Za-z_][A-Za-z0-9_]*)\})");
279 static const boost::regex reUnbraced(R"(\$([A-Za-z_][A-Za-z0-9_]*))");
280
281 // Track undefined variables so warnings appear only once per expansion
282 std::unordered_set<std::string> warned;
283
284 auto expand_match = [&](const boost::smatch& m) -> std::string
285 {
286 std::string var = m[1].str();
287 const char* v = std::getenv(var.c_str());
288
289 if (!v)
290 {
291 if (!warned.count(var))
292 {
293 std::cerr << "[EnvExpander] Warning: environment variable '" << var
294 << "' is not defined\n";
295 warned.insert(var);
296 }
297 return ""; // undefined → empty
298 }
299 return std::string(v);
300 };
301
302 result = boost::regex_replace(result, reBraced, expand_match);
303 result = boost::regex_replace(result, reUnbraced, expand_match);
304
305 return result;
306 }
307
308 void
309 EnvExpander::applyOperation(const std::string& name, Operation op, const std::string& val)
310 {
311 const char* existing = std::getenv(name.c_str());
312 std::string oldVal = (existing != nullptr) ? existing : "";
313
314 std::string newVal;
315
316 switch (op)
317 {
319 newVal = val;
320 break;
321
323 newVal = oldVal;
324 if (!oldVal.empty())
325 {
326 newVal += ":";
327 }
328 newVal += val;
329 break;
330
332 newVal = val;
333 if (!oldVal.empty())
334 {
335 newVal += ":";
336 }
337 newVal += oldVal;
338 break;
339 }
340
341 env_[name] = newVal;
342 setenv(name.c_str(), newVal.c_str(), 1);
343 }
344} // namespace armarx::core::system
static std::vector< std::string > splitAssignments(const std::string &input)
static std::string expand(const std::string &input)
Return the expanded form of the comma-separated assignment list without modifying the process environ...
EnvExpander()
Construct an EnvExpander initialized with the current process environment.
static std::tuple< std::string, Operation, std::string > splitOperation(const std::string &assignment)
boost::process::environment env_
Definition EnvExpander.h:86
void applyOperation(const std::string &name, Operation op, const std::string &val)
static std::string expandVariables(const std::string &value)
void apply(const std::string &input, bool expandExisting=true)
Parse and apply the comma-separated assignment list to the internal environment env_ and to the proce...
This file is part of ArmarX.
std::optional< Eigen::Matrix< EigenT, rows, cols, Eigen::ColMajor > > & assign(std::optional< Eigen::Matrix< EigenT, rows, cols, Eigen::ColMajor > > &lh, const std::optional< Eigen::Matrix< EigenT, rows, cols, Eigen::RowMajor > > &rh)
Definition conversions.h:16