Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 113 additions & 4 deletions src/clice.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,126 @@ struct CliOptions {
DECO_CFG_END();
};

auto search_in_path(std::string_view name) -> std::string {
const char* path_env = std::getenv("PATH");
if(!path_env || name.empty()) {
return std::string(name);
}

#ifdef _WIN32
constexpr char path_sep = ';';

// Get PATHEXT or use default extensions
std::vector<std::string> extensions;
const char* pathext_env = std::getenv("PATHEXT");
Comment on lines +91 to +101
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In standard C++, which header declares std::getenv, and is using std::getenvwithout including guaranteed to be portable?

💡 Result:

  • Header: In standard C++, std::getenv is declared in <cstdlib>. [1][2]
  • Portability: No. Calling std::getenv without including <cstdlib> is not guaranteed portable—it may “work” only because some other header (or your implementation) happens to include/declare it, but the standard doesn’t require that. You should include <cstdlib> yourself. [2][3]

Sources:
[1] cppreference: <cstdlib> header contents (lists getenv) (en.cppreference.com)
[2] cppreference: C++ headers / requirements about what headers provide (en.cppreference.com)
[3] Stack Overflow discussion noting non-portability of relying on indirect declarations (stackoverflow.com)

Citations:


Add <cstdlib> for std::getenv portability.

The code uses std::getenv without including <cstdlib>, which is not guaranteed to be portable across standard-compliant implementations.

Suggested fix
+#include <cstdlib>
 `#include` <expected>
 `#include` <filesystem>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/clice.cc` around lines 91 - 101, The file uses std::getenv in clice.cc
(see usage around path_env and pathext_env) but doesn't include <cstdlib>; add
an `#include` <cstdlib> to the top of the translation unit so std::getenv is
defined portably (keep existing includes and place <cstdlib> with the other
system headers).

if(pathext_env) {
std::string_view pathext_view(pathext_env);
size_t ext_start = 0;
while(ext_start < pathext_view.size()) {
size_t ext_end = pathext_view.find(';', ext_start);
if(ext_end == std::string_view::npos) {
ext_end = pathext_view.size();
}
std::string_view ext = pathext_view.substr(ext_start, ext_end - ext_start);
if(!ext.empty()) {
extensions.emplace_back(ext);
}
ext_start = ext_end + 1;
}
} else {
extensions = {".exe", ".cmd", ".bat", ".com"};
}

// Check if name already has an extension
bool has_extension = name.find('.') != std::string_view::npos;

auto is_executable = [](const std::filesystem::path& p) {
std::error_code ec;
auto status = std::filesystem::status(p, ec);
return !ec && std::filesystem::exists(status) && !std::filesystem::is_directory(status);
};
#else
constexpr char path_sep = ':';
auto is_executable = [](const std::filesystem::path& p) {
std::error_code ec;
auto status = std::filesystem::status(p, ec);
if(ec || !std::filesystem::exists(status) || std::filesystem::is_directory(status)) {
return false;
}
return (status.permissions() & (std::filesystem::perms::owner_exec |
std::filesystem::perms::group_exec |
std::filesystem::perms::others_exec)) != std::filesystem::perms::none;
};
#endif

std::string_view path_view(path_env);
size_t start = 0;
while(start < path_view.size()) {
size_t end = path_view.find(path_sep, start);
if(end == std::string_view::npos) {
end = path_view.size();
}

std::string_view dir = path_view.substr(start, end - start);
if(!dir.empty()) {
#ifdef _WIN32
// Try the name as-is first
std::filesystem::path full_path = std::filesystem::path(dir) / name;
std::error_code ec;
auto canonical = std::filesystem::canonical(full_path, ec);
if(!ec && is_executable(canonical)) {
return canonical.string();
}

// If name doesn't have an extension, try each PATHEXT extension
if(!has_extension) {
for(const auto& ext : extensions) {
std::string name_with_ext = std::string(name) + ext;
full_path = std::filesystem::path(dir) / name_with_ext;
canonical = std::filesystem::canonical(full_path, ec);
if(!ec && is_executable(canonical)) {
return canonical.string();
}
}
}
#else
std::filesystem::path full_path = std::filesystem::path(dir) / name;
std::error_code ec;
auto canonical = std::filesystem::canonical(full_path, ec);
if(!ec && is_executable(canonical)) {
return canonical.string();
}
#endif
}
Comment on lines +144 to +180
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

When resolving a bare command name, does Windows search the current directory before PATH, and on POSIX does an empty PATH component represent the current directory?

💡 Result:

  • Windows (Win32 process creation / “bare” executable name): Usually yes. When Windows performs an executable search (e.g., for a relative name), it searches the current working directory before the system PATH when SafeProcessSearchMode is 0 (the documented default). If SafeProcessSearchMode is set to 1 (or changed per-process), it searches the system PATH first and the current directory later. [1]

  • POSIX: Yes. In PATH, a zero-length (empty) component means the current working directory (written as ::, a leading :, or a trailing :). POSIX notes this is a “legacy feature”; strictly conforming applications should use . explicitly. [2]

Sources: [1] SearchPathA (Microsoft Learn) • [2] POSIX environment variable PATH (The Open Group)


🏁 Script executed:

# Check if the function has any current directory handling
cd src && grep -n "current_path\|current_directory\|\"\.\"\|dir\.empty()" clice.cc | head -30

Repository: clice-io/clice

Length of output: 88


🏁 Script executed:

# Find the search_in_path function and display it with context
cd src && grep -n "search_in_path" clice.cc | head -5

Repository: clice-io/clice

Length of output: 157


🏁 Script executed:

# Display the complete search_in_path function (approximately lines 90-210)
cd src && sed -n '90,210p' clice.cc

Repository: clice-io/clice

Length of output: 4406


Ensure current directory is checked as part of executable search.

The loop skips empty PATH entries (if(!dir.empty())), but on POSIX, an empty PATH component represents the current directory per POSIX specification and should be searched. On Windows, while the operating system searches the current directory before PATH by default, this behavior is not guaranteed—it depends on the SafeProcessSearchMode registry setting. The code should explicitly check the current directory to ensure consistent, predictable behavior across platforms. Treat empty PATH entries as "." on POSIX, and prepend the current directory (via std::filesystem::current_path()) on Windows before searching the PATH.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/clice.cc` around lines 144 - 180, The PATH loop currently skips empty
PATH components (if(!dir.empty())) which ignores the POSIX meaning of empty
entries as the current directory and does not explicitly search the CWD on
Windows; update the loop handling around path_view/path_sep so that when dir is
empty you treat it as the current directory (use "." on POSIX) or, on Windows,
explicitly insert std::filesystem::current_path() into the search before
iterating PATH entries; ensure this logic is applied where full_path is
constructed (std::filesystem::path(dir) / name) and that the existing canonical
+ is_executable checks (and the PATHEXT extension loop using extensions and
has_extension) are reused for these inserted current-directory checks.


start = end + 1;
}

return std::string(name);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

auto resolve_self_path(int argc, const char** argv) -> std::string {
if(argc <= 0 || argv == nullptr || argv[0] == nullptr) {
return "clice";
}

std::string_view arg0(argv[0]);
std::error_code ec;
auto absolute = std::filesystem::absolute(argv[0], ec);
if(ec) {
return std::string(argv[0]);

// If arg0 contains a path separator, treat it as a path (relative or absolute)
if(arg0.find('/') != std::string_view::npos || arg0.find('\\') != std::string_view::npos) {
auto absolute = std::filesystem::absolute(arg0, ec);
if(!ec) {
auto canonical = std::filesystem::weakly_canonical(absolute, ec);
if(!ec) {
return canonical.string();
}
}
return std::string(arg0);
}
return absolute.string();

// No path separator - search in PATH
return search_in_path(arg0);
}

auto build_options(const CliOptions& cli_options, int argc, const char** argv)
Expand Down
Loading