mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-26 12:53:13 +00:00
c78e56c509
Use __ instead of _ as a placeholder for ignored variables in `read` statements.
827 lines
20 KiB
C++
827 lines
20 KiB
C++
/** \file builtin_set.c Functions defining the set builtin
|
|
|
|
Functions used for implementing the set builtin.
|
|
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <wchar.h>
|
|
#include <wctype.h>
|
|
#include <sys/types.h>
|
|
#include <termios.h>
|
|
#include <signal.h>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include "fallback.h"
|
|
#include "util.h"
|
|
|
|
#include "wutil.h"
|
|
#include "builtin.h"
|
|
#include "env.h"
|
|
#include "expand.h"
|
|
#include "common.h"
|
|
#include "wgetopt.h"
|
|
#include "proc.h"
|
|
#include "parser.h"
|
|
|
|
/* We know about these buffers */
|
|
extern wcstring stdout_buffer, stderr_buffer;
|
|
|
|
/**
|
|
Error message for invalid path operations
|
|
*/
|
|
#define BUILTIN_SET_PATH_ERROR L"%ls: Warning: path component %ls may not be valid in %ls.\n"
|
|
|
|
/**
|
|
Hint for invalid path operation with a colon
|
|
*/
|
|
#define BUILTIN_SET_PATH_HINT L"%ls: Did you mean 'set %ls $%ls %ls'?\n"
|
|
|
|
/**
|
|
Error for mismatch between index count and elements
|
|
*/
|
|
#define BUILTIN_SET_ARG_COUNT L"%ls: The number of variable indexes does not match the number of values\n"
|
|
|
|
/**
|
|
Test if the specified variable should be subject to path validation
|
|
*/
|
|
static int is_path_variable(const wchar_t *env)
|
|
{
|
|
return contains(env, L"PATH", L"CDPATH");
|
|
}
|
|
|
|
/**
|
|
Call env_set. If this is a path variable, e.g. PATH, validate the
|
|
elements. On error, print a description of the problem to stderr.
|
|
*/
|
|
static int my_env_set(const wchar_t *key, const wcstring_list_t &val, int scope)
|
|
{
|
|
size_t i;
|
|
int retcode = 0;
|
|
const wchar_t *val_str=NULL;
|
|
|
|
if (is_path_variable(key))
|
|
{
|
|
/* Fix for https://github.com/fish-shell/fish-shell/issues/199 . Return success if any path setting succeeds. */
|
|
bool any_success = false;
|
|
|
|
/* Don't bother validating (or complaining about) values that are already present */
|
|
wcstring_list_t existing_values;
|
|
const env_var_t existing_variable = env_get_string(key, scope);
|
|
if (! existing_variable.missing_or_empty())
|
|
tokenize_variable_array(existing_variable, existing_values);
|
|
|
|
for (i=0; i< val.size() ; i++)
|
|
{
|
|
const wcstring &dir = val.at(i);
|
|
if (list_contains_string(existing_values, dir))
|
|
{
|
|
any_success = true;
|
|
continue;
|
|
}
|
|
|
|
bool show_perror = false;
|
|
int show_hint = 0;
|
|
bool error = false;
|
|
|
|
struct stat buff;
|
|
if (wstat(dir, &buff))
|
|
{
|
|
error = true;
|
|
show_perror = true;
|
|
}
|
|
|
|
if (!(S_ISDIR(buff.st_mode)))
|
|
{
|
|
error = true;
|
|
}
|
|
|
|
if (!error)
|
|
{
|
|
any_success = true;
|
|
}
|
|
else
|
|
{
|
|
append_format(stderr_buffer, _(BUILTIN_SET_PATH_ERROR), L"set", dir.c_str(), key);
|
|
const wchar_t *colon = wcschr(dir.c_str(), L':');
|
|
|
|
if (colon && *(colon+1))
|
|
{
|
|
show_hint = 1;
|
|
}
|
|
|
|
}
|
|
|
|
if (show_perror)
|
|
{
|
|
builtin_wperror(L"set");
|
|
}
|
|
|
|
if (show_hint)
|
|
{
|
|
append_format(stderr_buffer, _(BUILTIN_SET_PATH_HINT), L"set", key, key, wcschr(dir.c_str(), L':')+1);
|
|
}
|
|
|
|
}
|
|
|
|
/* Fail at setting the path if we tried to set it to something non-empty, but it wound up empty. */
|
|
if (! val.empty() && ! any_success)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
}
|
|
|
|
wcstring sb;
|
|
if (! val.empty())
|
|
{
|
|
for (i=0; i< val.size() ; i++)
|
|
{
|
|
sb.append(val[i]);
|
|
if (i<val.size() - 1)
|
|
{
|
|
sb.append(ARRAY_SEP_STR);
|
|
}
|
|
}
|
|
val_str = sb.c_str();
|
|
}
|
|
|
|
switch (env_set(key, val_str, scope | ENV_USER))
|
|
{
|
|
case ENV_PERM:
|
|
{
|
|
append_format(stderr_buffer, _(L"%ls: Tried to change the read-only variable '%ls'\n"), L"set", key);
|
|
retcode=1;
|
|
break;
|
|
}
|
|
|
|
case ENV_SCOPE:
|
|
{
|
|
append_format(stderr_buffer, _(L"%ls: Tried to set the special variable '%ls' with the wrong scope\n"), L"set", key);
|
|
retcode=1;
|
|
break;
|
|
}
|
|
|
|
case ENV_INVALID:
|
|
{
|
|
append_format(stderr_buffer, _(L"%ls: Tried to set the special variable '%ls' to an invalid value\n"), L"set", key);
|
|
retcode=1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return retcode;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
Extract indexes from a destination argument of the form name[index1 index2...]
|
|
|
|
\param indexes the list to insert the new indexes into
|
|
\param src the source string to parse
|
|
\param name the name of the element. Return null if the name in \c src does not match this name
|
|
\param var_count the number of elements in the array to parse.
|
|
|
|
\return the total number of indexes parsed, or -1 on error
|
|
*/
|
|
static int parse_index(std::vector<long> &indexes,
|
|
const wchar_t *src,
|
|
const wchar_t *name,
|
|
size_t var_count)
|
|
{
|
|
size_t len;
|
|
|
|
int count = 0;
|
|
const wchar_t *src_orig = src;
|
|
|
|
if (src == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
while (*src != L'\0' && (iswalnum(*src) || *src == L'_'))
|
|
{
|
|
src++;
|
|
}
|
|
|
|
if (*src != L'[')
|
|
{
|
|
append_format(stderr_buffer, _(BUILTIN_SET_ARG_COUNT), L"set");
|
|
return 0;
|
|
}
|
|
|
|
len = src-src_orig;
|
|
|
|
if ((wcsncmp(src_orig, name, len)!=0) || (wcslen(name) != (len)))
|
|
{
|
|
append_format(stderr_buffer,
|
|
_(L"%ls: Multiple variable names specified in single call (%ls and %.*ls)\n"),
|
|
L"set",
|
|
name,
|
|
len,
|
|
src_orig);
|
|
return 0;
|
|
}
|
|
|
|
src++;
|
|
|
|
while (iswspace(*src))
|
|
{
|
|
src++;
|
|
}
|
|
|
|
while (*src != L']')
|
|
{
|
|
wchar_t *end;
|
|
|
|
long l_ind;
|
|
|
|
errno = 0;
|
|
|
|
l_ind = wcstol(src, &end, 10);
|
|
|
|
if (end==src || errno)
|
|
{
|
|
append_format(stderr_buffer, _(L"%ls: Invalid index starting at '%ls'\n"), L"set", src);
|
|
return 0;
|
|
}
|
|
|
|
if (l_ind < 0)
|
|
{
|
|
l_ind = var_count+l_ind+1;
|
|
}
|
|
|
|
src = end;
|
|
if (*src==L'.' && *(src+1)==L'.')
|
|
{
|
|
src+=2;
|
|
long l_ind2 = wcstol(src, &end, 10);
|
|
if (end==src || errno)
|
|
{
|
|
return 1;
|
|
}
|
|
src = end;
|
|
|
|
if (l_ind2 < 0)
|
|
{
|
|
l_ind2 = var_count+l_ind2+1;
|
|
}
|
|
int direction = l_ind2<l_ind ? -1 : 1 ;
|
|
for (long jjj = l_ind; jjj*direction <= l_ind2*direction; jjj+=direction)
|
|
{
|
|
// debug(0, L"Expand range [set]: %i\n", jjj);
|
|
indexes.push_back(jjj);
|
|
count++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
indexes.push_back(l_ind);
|
|
count++;
|
|
}
|
|
while (iswspace(*src)) src++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static int update_values(wcstring_list_t &list,
|
|
std::vector<long> &indexes,
|
|
wcstring_list_t &values)
|
|
{
|
|
size_t i;
|
|
|
|
/* Replace values where needed */
|
|
for (i = 0; i < indexes.size(); i++)
|
|
{
|
|
/*
|
|
The '- 1' below is because the indices in fish are
|
|
one-based, but the vector uses zero-based indices
|
|
*/
|
|
long ind = indexes[i] - 1;
|
|
const wcstring newv = values[ i ];
|
|
if (ind < 0)
|
|
{
|
|
return 1;
|
|
}
|
|
if ((size_t)ind >= list.size())
|
|
{
|
|
list.resize(ind+1);
|
|
}
|
|
|
|
// free((void *) al_get(list, ind));
|
|
list[ ind ] = newv;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
Erase from a list of wcstring values at specified indexes
|
|
*/
|
|
static void erase_values(wcstring_list_t &list, const std::vector<long> &indexes)
|
|
{
|
|
// Make a set of indexes.
|
|
// This both sorts them into ascending order and removes duplicates.
|
|
const std::set<long> indexes_set(indexes.begin(), indexes.end());
|
|
|
|
// Now walk the set backwards, so we encounter larger indexes first, and remove elements at the given (1-based) indexes.
|
|
std::set<long>::const_reverse_iterator iter;
|
|
for (iter = indexes_set.rbegin(); iter != indexes_set.rend(); ++iter)
|
|
{
|
|
long val = *iter;
|
|
if (val > 0 && (size_t)val <= list.size())
|
|
{
|
|
// One-based indexing!
|
|
list.erase(list.begin() + val - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
Print the names of all environment variables in the scope, with or without shortening,
|
|
with or without values, with or without escaping
|
|
*/
|
|
static void print_variables(int include_values, int esc, bool shorten_ok, int scope)
|
|
{
|
|
wcstring_list_t names = env_get_names(scope);
|
|
sort(names.begin(), names.end());
|
|
|
|
for (size_t i = 0; i < names.size(); i++)
|
|
{
|
|
const wcstring key = names.at(i);
|
|
const wcstring e_key = escape_string(key, 0);
|
|
|
|
stdout_buffer.append(e_key);
|
|
|
|
if (include_values)
|
|
{
|
|
env_var_t value = env_get_string(key, scope);
|
|
if (!value.missing())
|
|
{
|
|
int shorten = 0;
|
|
|
|
if (shorten_ok && value.length() > 64)
|
|
{
|
|
shorten = 1;
|
|
value.resize(60);
|
|
}
|
|
|
|
wcstring e_value = esc ? expand_escape_variable(value) : value;
|
|
|
|
stdout_buffer.append(L" ");
|
|
stdout_buffer.append(e_value);
|
|
|
|
if (shorten)
|
|
{
|
|
stdout_buffer.push_back(ellipsis_char);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
stdout_buffer.append(L"\n");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
The set builtin. Creates, updates and erases environment variables
|
|
and environemnt variable arrays.
|
|
*/
|
|
static int builtin_set(parser_t &parser, wchar_t **argv)
|
|
{
|
|
/** Variables used for parsing the argument list */
|
|
const struct woption long_options[] =
|
|
{
|
|
{ L"export", no_argument, 0, 'x' },
|
|
{ L"global", no_argument, 0, 'g' },
|
|
{ L"local", no_argument, 0, 'l' },
|
|
{ L"erase", no_argument, 0, 'e' },
|
|
{ L"names", no_argument, 0, 'n' },
|
|
{ L"unexport", no_argument, 0, 'u' },
|
|
{ L"universal", no_argument, 0, 'U' },
|
|
{ L"long", no_argument, 0, 'L' },
|
|
{ L"query", no_argument, 0, 'q' },
|
|
{ L"help", no_argument, 0, 'h' },
|
|
{ 0, 0, 0, 0 }
|
|
} ;
|
|
|
|
const wchar_t *short_options = L"+xglenuULqh";
|
|
|
|
int argc = builtin_count_args(argv);
|
|
|
|
/*
|
|
Flags to set the work mode
|
|
*/
|
|
int local = 0, global = 0, exportv = 0;
|
|
int erase = 0, list = 0, unexport=0;
|
|
int universal = 0, query=0;
|
|
bool shorten_ok = true;
|
|
bool preserve_incoming_failure_exit_status = true;
|
|
const int incoming_exit_status = proc_get_last_status();
|
|
|
|
/*
|
|
Variables used for performing the actual work
|
|
*/
|
|
wchar_t *dest = 0;
|
|
int retcode=0;
|
|
int scope;
|
|
int slice=0;
|
|
int i;
|
|
|
|
const wchar_t *bad_char = NULL;
|
|
|
|
|
|
/* Parse options to obtain the requested operation and the modifiers */
|
|
woptind = 0;
|
|
while (1)
|
|
{
|
|
int c = wgetopt_long(argc, argv, short_options, long_options, 0);
|
|
|
|
if (c == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
switch (c)
|
|
{
|
|
case 0:
|
|
break;
|
|
|
|
case 'e':
|
|
erase = 1;
|
|
preserve_incoming_failure_exit_status = false;
|
|
break;
|
|
|
|
case 'n':
|
|
list = 1;
|
|
preserve_incoming_failure_exit_status = false;
|
|
break;
|
|
|
|
case 'x':
|
|
exportv = 1;
|
|
break;
|
|
|
|
case 'l':
|
|
local = 1;
|
|
break;
|
|
|
|
case 'g':
|
|
global = 1;
|
|
break;
|
|
|
|
case 'u':
|
|
unexport = 1;
|
|
break;
|
|
|
|
case 'U':
|
|
universal = 1;
|
|
break;
|
|
|
|
case 'L':
|
|
shorten_ok = false;
|
|
break;
|
|
|
|
case 'q':
|
|
query = 1;
|
|
preserve_incoming_failure_exit_status = false;
|
|
break;
|
|
|
|
case 'h':
|
|
builtin_print_help(parser, argv[0], stdout_buffer);
|
|
return 0;
|
|
|
|
case '?':
|
|
builtin_unknown_option(parser, argv[0], argv[woptind-1]);
|
|
return 1;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Ok, all arguments have been parsed, let's validate them
|
|
*/
|
|
|
|
/*
|
|
If we are checking the existance of a variable (-q) we can not
|
|
also specify scope
|
|
*/
|
|
|
|
if (query && (erase || list))
|
|
{
|
|
append_format(stderr_buffer,
|
|
BUILTIN_ERR_COMBO,
|
|
argv[0]);
|
|
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
return 1;
|
|
}
|
|
|
|
|
|
/* We can't both list and erase variables */
|
|
if (erase && list)
|
|
{
|
|
append_format(stderr_buffer,
|
|
BUILTIN_ERR_COMBO,
|
|
argv[0]);
|
|
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
Variables can only have one scope
|
|
*/
|
|
if (local + global + universal > 1)
|
|
{
|
|
append_format(stderr_buffer,
|
|
BUILTIN_ERR_GLOCAL,
|
|
argv[0]);
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
Variables can only have one export status
|
|
*/
|
|
if (exportv && unexport)
|
|
{
|
|
append_format(stderr_buffer,
|
|
BUILTIN_ERR_EXPUNEXP,
|
|
argv[0]);
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
Calculate the scope value for variable assignement
|
|
*/
|
|
scope = (local ? ENV_LOCAL : 0) | (global ? ENV_GLOBAL : 0) | (exportv ? ENV_EXPORT : 0) | (unexport ? ENV_UNEXPORT : 0) | (universal ? ENV_UNIVERSAL:0) | ENV_USER;
|
|
|
|
if (query)
|
|
{
|
|
/*
|
|
Query mode. Return the number of variables that do not exist
|
|
out of the specified variables.
|
|
*/
|
|
int i;
|
|
for (i=woptind; i<argc; i++)
|
|
{
|
|
wchar_t *arg = argv[i];
|
|
int slice=0;
|
|
|
|
if (!(dest = wcsdup(arg)))
|
|
{
|
|
DIE_MEM();
|
|
}
|
|
|
|
if (wcschr(dest, L'['))
|
|
{
|
|
slice = 1;
|
|
*wcschr(dest, L'[')=0;
|
|
}
|
|
|
|
if (slice)
|
|
{
|
|
std::vector<long> indexes;
|
|
wcstring_list_t result;
|
|
size_t j;
|
|
|
|
env_var_t dest_str = env_get_string(dest, scope);
|
|
if (! dest_str.missing())
|
|
tokenize_variable_array(dest_str, result);
|
|
|
|
if (!parse_index(indexes, arg, dest, result.size()))
|
|
{
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
retcode = 1;
|
|
break;
|
|
}
|
|
for (j=0; j < indexes.size() ; j++)
|
|
{
|
|
long idx = indexes[j];
|
|
if (idx < 1 || (size_t)idx > result.size())
|
|
{
|
|
retcode++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!env_exist(arg, scope))
|
|
{
|
|
retcode++;
|
|
}
|
|
}
|
|
|
|
free(dest);
|
|
|
|
}
|
|
return retcode;
|
|
}
|
|
|
|
if (list)
|
|
{
|
|
/* Maybe we should issue an error if there are any other arguments? */
|
|
print_variables(0, 0, shorten_ok, scope);
|
|
return 0;
|
|
}
|
|
|
|
if (woptind == argc)
|
|
{
|
|
/*
|
|
Print values of variables
|
|
*/
|
|
|
|
if (erase)
|
|
{
|
|
append_format(stderr_buffer,
|
|
_(L"%ls: Erase needs a variable name\n"),
|
|
argv[0]);
|
|
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
retcode = 1;
|
|
}
|
|
else
|
|
{
|
|
print_variables(1, 1, shorten_ok, scope);
|
|
}
|
|
|
|
return retcode;
|
|
}
|
|
|
|
if (!(dest = wcsdup(argv[woptind])))
|
|
{
|
|
DIE_MEM();
|
|
}
|
|
|
|
if (wcschr(dest, L'['))
|
|
{
|
|
slice = 1;
|
|
*wcschr(dest, L'[')=0;
|
|
}
|
|
|
|
if (!wcslen(dest))
|
|
{
|
|
free(dest);
|
|
append_format(stderr_buffer, BUILTIN_ERR_VARNAME_ZERO, argv[0]);
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
return 1;
|
|
}
|
|
|
|
if ((bad_char = wcsvarname(dest)))
|
|
{
|
|
append_format(stderr_buffer, BUILTIN_ERR_VARCHAR, argv[0], *bad_char);
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
free(dest);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
set assignment can work in two modes, either using slices or
|
|
using the whole array. We detect which mode is used here.
|
|
*/
|
|
|
|
if (slice)
|
|
{
|
|
|
|
/*
|
|
Slice mode
|
|
*/
|
|
std::vector<long> indexes;
|
|
wcstring_list_t result;
|
|
|
|
const env_var_t dest_str = env_get_string(dest, scope);
|
|
if (! dest_str.missing())
|
|
{
|
|
tokenize_variable_array(dest_str, result);
|
|
}
|
|
else if (erase)
|
|
{
|
|
retcode = 1;
|
|
}
|
|
|
|
if (!retcode)
|
|
{
|
|
for (; woptind<argc; woptind++)
|
|
{
|
|
if (!parse_index(indexes, argv[woptind], dest, result.size()))
|
|
{
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
retcode = 1;
|
|
break;
|
|
}
|
|
|
|
size_t idx_count = indexes.size();
|
|
size_t val_count = argc-woptind-1;
|
|
|
|
if (!erase)
|
|
{
|
|
if (val_count < idx_count)
|
|
{
|
|
append_format(stderr_buffer, _(BUILTIN_SET_ARG_COUNT), argv[0]);
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
retcode=1;
|
|
break;
|
|
}
|
|
if (val_count == idx_count)
|
|
{
|
|
woptind++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!retcode)
|
|
{
|
|
/*
|
|
Slice indexes have been calculated, do the actual work
|
|
*/
|
|
|
|
if (erase)
|
|
{
|
|
erase_values(result, indexes);
|
|
my_env_set(dest, result, scope);
|
|
}
|
|
else
|
|
{
|
|
wcstring_list_t value;
|
|
|
|
while (woptind < argc)
|
|
{
|
|
value.push_back(argv[woptind++]);
|
|
}
|
|
|
|
if (update_values(result,
|
|
indexes,
|
|
value))
|
|
{
|
|
append_format(stderr_buffer, L"%ls: ", argv[0]);
|
|
append_format(stderr_buffer, ARRAY_BOUNDS_ERR);
|
|
stderr_buffer.push_back(L'\n');
|
|
}
|
|
|
|
my_env_set(dest, result, scope);
|
|
|
|
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
woptind++;
|
|
|
|
/*
|
|
No slicing
|
|
*/
|
|
if (erase)
|
|
{
|
|
if (woptind != argc)
|
|
{
|
|
append_format(stderr_buffer,
|
|
_(L"%ls: Values cannot be specfied with erase\n"),
|
|
argv[0]);
|
|
builtin_print_help(parser, argv[0], stderr_buffer);
|
|
retcode=1;
|
|
}
|
|
else
|
|
{
|
|
retcode = env_remove(dest, scope);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wcstring_list_t val;
|
|
for (i=woptind; i<argc; i++)
|
|
val.push_back(argv[i]);
|
|
retcode = my_env_set(dest, val, scope);
|
|
}
|
|
}
|
|
|
|
/* Check if we are setting variables above the effective scope.
|
|
See https://github.com/fish-shell/fish-shell/issues/806
|
|
*/
|
|
|
|
env_var_t global_dest = env_get_string(dest, ENV_GLOBAL);
|
|
if (universal && ! global_dest.missing())
|
|
{
|
|
append_format(stderr_buffer, _(L"%ls: Warning: universal scope selected, but a global variable '%ls' exists.\n"), L"set", dest);
|
|
}
|
|
|
|
free(dest);
|
|
|
|
if (retcode == STATUS_BUILTIN_OK && preserve_incoming_failure_exit_status)
|
|
retcode = incoming_exit_status;
|
|
return retcode;
|
|
|
|
}
|
|
|