sortix-mirror/sh/sh.c

2397 lines
55 KiB
C

/*
* Copyright (c) 2011-2016, 2022 Jonas 'Sortie' Termansen.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* sh.c
* Command language interpreter.
*/
#include <sys/stat.h>
#include <sys/wait.h>
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <glob.h>
#include <inttypes.h>
#include <ioleast.h>
#include <libgen.h>
#include <limits.h>
#include <locale.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>
// Sortix libc doesn't have its own proper <limits.h> at this time.
#if defined(__sortix__)
#include <sortix/limits.h>
#endif
#include "editline.h"
#include "showline.h"
#include "util.h"
static const char* builtin_commands[] =
{
"cd",
"exit",
"unset",
"clearenv",
"history",
(const char*) NULL,
};
static bool foreground_shell;
static int status = 0;
static struct edit_line edit_state;
static bool is_proper_absolute_path(const char* path)
{
if ( path[0] == '\0' )
return false;
if ( path[0] != '/' )
return false;
while ( path[0] )
{
if ( path[0] == '/' )
path++;
else if ( path[0] == '.' &&
(path[1] == '\0' || path[1] == '/') )
return false;
else if ( path[0] == '.' &&
path[1] == '.' &&
(path[2] == '\0' || path[2] == '/') )
return false;
else
{
while ( *path && *path != '/' )
path++;
}
}
return true;
}
void update_env(void)
{
char str[3 * sizeof(size_t)];
struct winsize ws;
if ( tcgetwinsize(0, &ws) == 0 )
{
snprintf(str, sizeof(str), "%zu", (size_t) ws.ws_col);
setenv("COLUMNS", str, 1);
snprintf(str, sizeof(str), "%zu", (size_t) ws.ws_row);
setenv("LINES", str, 1);
}
}
void array_shrink_free(void*** array_ptr,
size_t* used_ptr,
size_t* length_ptr,
size_t new_used)
{
void** array;
memcpy(&array, array_ptr, sizeof(array)); // Strict aliasing.
for ( size_t i = new_used; i < *used_ptr; i++ )
{
void* value;
memcpy(&value, array + i, sizeof(value)); // Strict aliasing.
free(value);
}
*used_ptr = new_used;
(void) length_ptr;
}
char* token_finalize(const char* token)
{
struct stringbuf buf;
stringbuf_begin(&buf);
bool escape = false;
bool single_quote = false;
bool double_quote = false;
for ( size_t i = 0; token[i]; i++ )
{
if ( !escape && !single_quote && token[i] == '\\' )
{
escape = true;
}
else if ( !escape && !double_quote && token[i] == '\'' )
{
single_quote = !single_quote;
}
else if ( !escape && !single_quote && token[i] == '"' )
{
double_quote = !double_quote;
}
else if ( escape && token[i] == '\n' )
{
escape = false;
}
else
{
if ( escape && double_quote &&
token[i] != '$' && token[i] != '`' &&
token[i] != '"' && token[i] != '\\' )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, token[i]);
escape = false;
}
}
return stringbuf_finish(&buf);
}
bool is_identifier_char(char c)
{
return ('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '_';
}
char* token_expand_variables(const char* token)
{
struct stringbuf buf;
stringbuf_begin(&buf);
bool escape = false;
bool single_quote = false;
bool double_quote = false;
for ( size_t i = 0; token[i]; i++ )
{
if ( !escape && !single_quote && token[i] == '\\' )
{
stringbuf_append_c(&buf, '\\');
escape = true;
}
else if ( !escape && !double_quote && token[i] == '\'' )
{
stringbuf_append_c(&buf, '\'');
single_quote = !single_quote;
}
else if ( !escape && !single_quote && token[i] == '"' )
{
stringbuf_append_c(&buf, '"');
double_quote = !double_quote;
}
else if ( !escape && !single_quote && token[i] == '$' && token[i + 1] )
{
i++;
const char* value;
if ( token[i] == '{' )
{
i++;
size_t length = 0;
while ( token[i + length] && token[i + length] != '}' )
length++;
char* variable = strndup(token + i, length);
if ( !variable )
return free(buf.string), (char*) NULL;
value = getenv(variable);
free(variable);
i += length;
if ( token[i] == '}' )
i++;
i--;
}
else if ( is_identifier_char(token[i]) )
{
size_t length = 1;
while ( is_identifier_char(token[i + length]) )
length++;
char* variable = strndup(token + i, length);
if ( !variable )
return free(buf.string), (char*) NULL;
value = getenv(variable);
free(variable);
i += length - 1;
}
else
{
char variable[2] = { token[i], '\0' };
value = getenv(variable);
}
for ( size_t n = 0; value && value[n]; n++ )
{
if ( double_quote && might_need_shell_quote(value[i]) )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, value[n]);
}
}
else
{
if ( escape && double_quote &&
token[i] != '$' && token[i] != '`' &&
token[i] != '"' && token[i] != '\\' )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, token[i]);
escape = false;
}
}
return stringbuf_finish(&buf);
}
bool token_split(void*** out,
size_t* out_used,
size_t* out_length,
const char* token)
{
size_t old_used = *out_used;
size_t index = 0;
while ( true )
{
while ( token[index] && isspace((unsigned char) token[index]) )
index++;
if ( !token[index] )
break;
struct stringbuf buf;
stringbuf_begin(&buf);
bool escape = false;
bool single_quote = false;
bool double_quote = false;
for ( ; token[index]; index++ )
{
if ( !escape && !single_quote && token[index] == '\\' )
{
stringbuf_append_c(&buf, '\\');
escape = true;
}
else if ( !escape && !double_quote && token[index] == '\'' )
{
stringbuf_append_c(&buf, '\'');
single_quote = !single_quote;
}
else if ( !escape && !single_quote && token[index] == '"' )
{
stringbuf_append_c(&buf, '"');
double_quote = !double_quote;
}
else if ( !(escape || single_quote || double_quote) &&
isspace((unsigned char) token[index]) )
{
break;
}
else if ( escape && token[index] == '\n' )
{
escape = false;
}
else
{
if ( escape && double_quote &&
token[index] != '$' && token[index] != '`' &&
token[index] != '"' && token[index] != '\\' )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, token[index]);
escape = false;
}
}
char* value = stringbuf_finish(&buf);
if ( !value )
{
array_shrink_free(out, out_used, out_length, old_used);
return false;
}
if ( !array_add(out, out_used, out_length, value) )
{
array_shrink_free(out, out_used, out_length, old_used);
return false;
}
}
return true;
}
bool token_expand_variables_split(void*** out,
size_t* out_used,
size_t* out_length,
const char* token)
{
char* expanded = token_expand_variables(token);
if ( !expanded )
return false;
bool result = token_split(out, out_used, out_length, expanded);
free(expanded);
return result;
}
bool token_expand_wildcards(void*** out,
size_t* out_used,
size_t* out_length,
const char* token)
{
size_t old_used = *out_used;
struct stringbuf buf;
stringbuf_begin(&buf);
// First check if the token contains any wildcards at all.
bool escape = false;
bool single_quote = false;
bool double_quote = false;
bool any_wildcards = false;
for ( size_t i = 0; token[i]; i++ )
{
char c = token[i];
if ( !escape && !single_quote && c == '\\' )
escape = true;
else if ( !escape && !double_quote && c == '\'' )
single_quote = !single_quote;
else if ( !escape && !single_quote && c == '"' )
double_quote = !double_quote;
else if ( !(escape || single_quote || double_quote) &&
(c == '?' || c == '*' || c == '[') )
{
any_wildcards = true;
stringbuf_append_c(&buf, c);
}
else
{
if ( escape && double_quote &&
c != '$' && c != '`' && c != '"' && c != '\\' )
stringbuf_append_c(&buf, '\\');
else if ( (escape || single_quote || double_quote) &&
(c == '?' || c == '*' || c == '[') )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, c);
escape = false;
}
}
char* pattern = stringbuf_finish(&buf);
if ( !pattern )
return false;
// If the token didn't contain any wildcards, just return it.
if ( !any_wildcards )
{
free(pattern);
just_return_input:
pattern = strdup(token);
if ( !pattern )
return false;
if ( !array_add(out, out_used, out_length, pattern) )
return free(pattern), false;
return true;
}
// Search the filesystem for paths matching the pattern.
glob_t gl;
int globerr = glob(pattern, 0, NULL, &gl);
free(pattern);
if ( globerr )
{
globfree(&gl);
// GLOB_NOCHECK is not used since we don't want the escaped pattern back
// since it would contain e.g. \* which is difficult to discern from a
// real file actually called \* and the original token is escaped in the
// correct fashion.
if ( globerr == GLOB_NOMATCH )
goto just_return_input;
return false;
}
// Escape the paths as tokens.
for ( size_t n = 0; n < gl.gl_pathc; n++ )
{
const char* path = gl.gl_pathv[n];
stringbuf_begin(&buf);
for ( size_t i = 0; path[i]; i++ )
{
if ( path[i] == '\n' )
{
stringbuf_append_c(&buf, '\'');
stringbuf_append_c(&buf, '\n');
stringbuf_append_c(&buf, '\'');
}
else
{
if ( might_need_shell_quote(path[i]) )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, path[i]);
}
}
char* new_token = stringbuf_finish(&buf);
if ( !new_token )
{
globfree(&gl);
array_shrink_free(out, out_used, out_length, old_used);
return false;
}
if ( !array_add(out, out_used, out_length, new_token) )
{
free(new_token);
globfree(&gl);
array_shrink_free(out, out_used, out_length, old_used);
return false;
}
}
globfree(&gl);
return true;
}
enum sh_tokenize_result
{
SH_TOKENIZE_RESULT_OK,
SH_TOKENIZE_RESULT_PARTIAL,
SH_TOKENIZE_RESULT_INVALID,
SH_TOKENIZE_RESULT_ERROR,
};
bool can_continue_operator(const char* op, char c)
{
if ( !op )
return false;
if ( op[0] == '<' && op[1] == '<' && op[2] == '\0' )
return c == '-';
if ( op[0] == '|' && op[1] == '\0' )
return c == '|';
if ( op[0] == '&' && op[1] == '\0' )
return c == '&';
if ( op[0] == ';' && op[1] == '\0' )
return c == ';';
if ( op[0] == '<' && op[1] == '\0' )
return c == '<' || c == '&' || c == '>';
if ( op[0] == '>' && op[1] == '\0' )
return c == '>' || c == '&' || c == '|';
if ( op[0] == '\0' )
return c == '|' || c == '&' || c == ';' || c == '>' || c == '<' ||
c == '(' || c == ')';
return false;
}
enum sh_tokenize_result sh_tokenize(const char* command,
char*** tokens_ptr,
size_t* tokens_used_ptr,
size_t* tokens_length_ptr)
{
enum sh_tokenize_result result = SH_TOKENIZE_RESULT_OK;
char** tokens = NULL;
size_t tokens_used = 0;
size_t tokens_length = 0;
size_t command_index = 0;
while ( true )
{
if ( command[command_index] == '\0' )
break;
if ( isspace((unsigned char) command[command_index]) )
{
command_index++;
continue;
}
if ( command[command_index] == '#' )
{
while ( command[command_index] != '\0' &&
command[command_index] != '\n' )
command_index++;
continue;
}
struct stringbuf buf;
stringbuf_begin(&buf);
bool escaped = false;
bool single_quote = false;
bool double_quote = false;
bool stop = false;
bool making_operator = false;
while ( true )
{
if ( command[command_index] == '\0' )
{
if ( escaped || single_quote || double_quote )
result = SH_TOKENIZE_RESULT_PARTIAL;
stop = true;
break;
}
else if ( making_operator )
{
if ( can_continue_operator(buf.string, command[command_index]) )
{
stringbuf_append_c(&buf, command[command_index]);
command_index++;
}
else
{
break;
}
}
else if ( buf.string && buf.length == 0 &&
can_continue_operator("", command[command_index]) )
{
stringbuf_append_c(&buf, command[command_index]);
making_operator = true;
command_index++;
}
else if ( !escaped && !single_quote && command[command_index] == '\\' )
{
stringbuf_append_c(&buf, '\\');
escaped = true;
command_index++;
}
else if ( !escaped && !double_quote && command[command_index] == '\'' )
{
stringbuf_append_c(&buf, '\'');
single_quote = !single_quote;
command_index++;
}
else if ( !escaped && !single_quote && command[command_index] == '"' )
{
stringbuf_append_c(&buf, '"');
double_quote = !double_quote;
command_index++;
}
else if ( !(escaped || single_quote || double_quote) &&
(isspace((unsigned char) command[command_index]) ||
can_continue_operator("", command[command_index])) )
{
break;
}
else if ( escaped && command[command_index] == '\n' )
{
if ( buf.string && buf.length && buf.string[buf.length - 1] == '\\' )
buf.string[--buf.length] = '\0';
command_index++;
escaped = false;
}
else
{
if ( escaped && double_quote &&
command[command_index] != '$' &&
command[command_index] != '`' &&
command[command_index] != '"' &&
command[command_index] != '\\' )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, command[command_index]);
command_index++;
escaped = false;
}
}
char* token = stringbuf_finish(&buf);
if ( !token )
{
result = SH_TOKENIZE_RESULT_ERROR;
break;
}
if ( !array_add((void***) &tokens,
&tokens_used,
&tokens_length,
token) )
{
free(token);
result = SH_TOKENIZE_RESULT_ERROR;
break;
}
if ( stop )
break;
}
*tokens_ptr = tokens;
*tokens_used_ptr = tokens_used;
*tokens_length_ptr = tokens_length;
return result;
}
bool is_shell_input_ready(const char* input)
{
char** tokens = NULL;
size_t tokens_used = 0;
size_t tokens_length = 0;
enum sh_tokenize_result tokenize_result =
sh_tokenize(input, &tokens, &tokens_used, &tokens_length);
bool result = tokenize_result == SH_TOKENIZE_RESULT_OK;
for ( size_t i = 0; i < tokens_used; i++ )
free(tokens[i]);
free(tokens);
return result;
}
int lexical_chdir(char* path)
{
assert(path[0] == '/');
int fd = open("/", O_RDONLY | O_DIRECTORY);
if ( fd < 0 )
return -1;
size_t input_index = 1;
size_t output_index = 1;
while ( path[input_index] )
{
if ( path[input_index] == '/' )
{
if ( output_index && path[output_index-1] != '/' )
path[output_index++] = path[input_index];
input_index++;
continue;
}
char* elem = path + input_index;
size_t elem_length = strcspn(elem, "/");
char lc = elem[elem_length];
elem[elem_length] = '\0';
if ( !strcmp(elem, ".") )
{
elem[elem_length] = lc;
input_index += elem_length;
continue;
}
if ( !strcmp(elem, "..") )
{
elem[elem_length] = lc;
input_index += elem_length;
if ( 2 <= output_index && path[output_index-1] == '/' )
output_index--;
while ( 2 <= output_index && path[output_index-1] != '/' )
output_index--;
if ( 2 <= output_index && path[output_index-1] == '/' )
output_index--;
lc = path[output_index];
path[output_index] = '\0';
int new_fd = open(path, O_RDONLY | O_DIRECTORY);
close(fd);
if ( new_fd < 0 )
return -1;
fd = new_fd;
path[output_index] = lc;
continue;
}
if ( 0 <= fd )
{
int new_fd = openat(fd, elem, O_RDONLY | O_DIRECTORY);
if ( new_fd < 0 )
close(fd);
fd = new_fd;
}
for ( size_t i = 0; i < elem_length; i++ )
path[output_index++] = path[input_index++];
elem[elem_length] = lc;
}
path[output_index] = '\0';
if ( 2 <= output_index && path[output_index-1] == '/' )
path[--output_index] = '\0';
int fchdir_ret = fchdir(fd);
close(fd);
if ( fchdir_ret < 0 )
return -1;
unsetenv("PWD");
setenv("PWD", path, 1);
return 0;
}
int perform_chdir(const char* path)
{
if ( !path[0] )
return errno = ENOENT, -1;
char* lexical_path = NULL;
if ( path[0] == '/' )
lexical_path = strdup(path);
else
{
char* current_pwd = get_current_dir_name();
if ( current_pwd )
{
assert(current_pwd[0] == '/');
asprintf(&lexical_path, "%s/%s", current_pwd, path);
free(current_pwd);
}
else if ( getenv("PWD") )
{
asprintf(&lexical_path, "/%s/%s", getenv("PWD"), path);
}
}
if ( lexical_path )
{
int ret = lexical_chdir(lexical_path);
free(lexical_path);
if ( ret == 0 )
return 0;
}
return chdir(path);
}
bool is_variable_assignment_token(const char* token)
{
size_t i = 0;
while ( is_identifier_char(token[i]) )
i++;
return i != 0 && token[i] == '=';
}
struct execute_result
{
pid_t pid;
int internal_status;
bool failure;
bool critical;
bool internal;
bool exited;
};
struct execute_result execute(char** tokens,
size_t tokens_count,
bool interactive,
int pipein,
int pipeout,
pid_t pgid)
{
char** varsv;
size_t varsc;
size_t varsv_allocated;
char** expandv;
size_t expandc;
size_t expandv_allocated;
char** argv;
size_t argc;
size_t argv_allocated;
bool internal;
bool failure = false;
bool critical = false;
bool do_exit = false;
bool set_pipein = false;
bool set_pipeout = false;
bool had_not_varassign = false;
pid_t childpid;
update_env();
char statusstr[sizeof(int) * 3];
snprintf(statusstr, sizeof(statusstr), "%i", status);
setenv("?", statusstr, 1);
varsv = NULL;
varsc = 0;
varsv_allocated = 0;
expandv = NULL;
expandc = 0;
expandv_allocated = 0;
for ( size_t i = 0; !failure && i < tokens_count; i++ )
{
if ( !had_not_varassign && is_variable_assignment_token(tokens[i]))
{
char* value = token_expand_variables(tokens[i]);
if ( !value )
{
error(0, errno, "variable expansion");
failure = true;
critical = true;
break;
}
if ( !array_add((void***) &varsv,
&varsc,
&varsv_allocated,
value) )
{
free(value);
error(0, errno, "variable expansion");
failure = true;
critical = true;
break;
}
}
else
{
had_not_varassign = true;
if ( !token_expand_variables_split((void***) &expandv,
&expandc,
&expandv_allocated,
tokens[i]) )
{
error(0, errno, "variable expansion");
failure = true;
critical = true;
break;
}
}
}
argv = NULL;
argc = 0;
argv_allocated = 0;
for ( size_t i = 0; !failure && i < expandc; i++ )
{
if ( !strcmp(expandv[i], "<") ||
!strcmp(expandv[i], ">") ||
/*!strcmp(expandv[i], "<<") ||*/
!strcmp(expandv[i], ">>") )
{
const char* type = expandv[i++];
if ( i == expandc )
{
error(0, errno, "%s: expected argument", type);
failure = true;
critical = true;
break;
}
char** targets = NULL;
size_t targets_used = 0;
size_t targets_length = 0;
if ( !token_expand_wildcards((void***) &targets,
&targets_used,
&targets_length,
expandv[i]) )
{
error(0, errno, "wildcard expansion");
failure = true;
critical = true;
break;
}
if ( targets_used != 1 )
{
error(0, 0, "%s: ambiguous redirect: %s", type, expandv[i]);
for ( size_t i = 0; i < targets_used; i++ )
free(targets[i]);
free(targets);
failure = true;
break;
}
char* target = token_finalize(targets[0]);
free(targets[0]);
free(targets);
if ( !target )
{
error(0, errno, "token finalization");
failure = true;
break;
}
int fd = -1;
if ( !strcmp(type, "<") )
fd = open(target, O_RDONLY | O_CLOEXEC);
else if ( !strcmp(type, ">") )
fd = open(target, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0666);
else if ( !strcmp(type, ">>") )
fd = open(target, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0666);
if ( fd < 0 )
{
error(0, errno, "%s", target);
free(target);
failure = true;
break;
}
if ( !strcmp(type, "<") )
{
pipein = fd;
set_pipein = true;
}
else if ( !strcmp(type, ">") || !strcmp(type, ">>") )
{
pipeout = fd;
set_pipeout = true;
}
free(target);
}
else
{
if ( !token_expand_wildcards((void***) &argv,
&argc,
&argv_allocated,
expandv[i]) )
{
error(0, errno, "wildcard expansion");
failure = true;
critical = true;
break;
}
}
}
for ( size_t i = 0; i < expandc; i++ )
free(expandv[i]);
free(expandv);
if ( !array_add((void***) &argv, &argc, &argv_allocated, (char*) NULL) )
{
failure = true;
critical = true;
}
else
{
argc--; // Don't count the NULL.
}
for ( size_t i = 0; i < varsc; i++ )
{
char* var = token_finalize(varsv[i]);
if ( !var )
{
error(0, errno, "token finalization");
failure = true;
break;
}
free(varsv[i]);
varsv[i] = var;
}
for ( size_t i = 0; i < argc; i++ )
{
char* arg = token_finalize(argv[i]);
if ( !arg )
{
error(0, errno, "token finalization");
failure = true;
break;
}
free(argv[i]);
argv[i] = arg;
}
// TODO: Builtins should be run in a child process if & or |.
childpid = getpid();
int internal_status = status;
if ( failure )
{
internal = true;
internal_status = 1;
}
else if ( argc == 0 )
{
internal = true;
for ( size_t i = 0; i < varsc; i++ )
{
char* key = varsv[i];
char* eq = strchr(key, '=');
if ( !eq )
continue;
*eq = '\0';
char* value = eq + 1;
int ret = setenv(key, value, 1);
*eq = '=';
if ( ret < 0 )
{
error(0, errno, "setenv");
internal_status = 1;
}
}
}
else if ( strcmp(argv[0], "cd") == 0 )
{
internal = true;
const char* newdir = argv[1];
if ( !newdir )
newdir = getenv_safe_def("HOME", "/");
internal_status = 0;
if ( perform_chdir(newdir) < 0 )
{
error(0, errno, "cd: %s", newdir);
internal_status = 1;
}
}
else if ( strcmp(argv[0], "exit") == 0 )
{
internal = true;
int exitcode = argv[1] ? atoi(argv[1]) : 0;
exitcode = (unsigned char) exitcode;
do_exit = true;
internal_status = exitcode;
}
else if ( strcmp(argv[0], "export") == 0 )
{
internal = true;
internal_status = 0;
if ( argv[1] )
{
size_t eqpos = strcspn(argv[1], "=");
if ( argv[1][eqpos] == '=' )
{
char* name = strndup(argv[1], eqpos);
if ( name )
{
if ( setenv(name, argv[1] + eqpos + 1, 1) < 0 )
{
error(0, errno, "export: setenv");
internal_status = 1;
}
}
else
{
error(0, errno, "export: malloc");
internal_status = 1;
}
}
}
else
{
for ( size_t i = 0; environ[i]; i++ )
{
const char* envvar = environ[i];
printf("export ");
while ( *envvar && *envvar != '=' )
putchar((unsigned char) *envvar++);
if ( *envvar == '=' )
putchar((unsigned char) *envvar++);
putchar('\'');
while ( *envvar )
{
if ( *envvar == '\'' )
{
putchar('\'');
putchar('\\');
putchar((unsigned char) *envvar++);
putchar('\'');
}
else
{
putchar((unsigned char) *envvar++);
}
}
putchar('\'');
putchar('\n');
}
}
}
else if ( strcmp(argv[0], "unset") == 0 )
{
internal = true;
unsetenv(argv[1] ? argv[1] : "");
internal_status = 0;
}
else if ( strcmp(argv[0], "clearenv") == 0 )
{
internal = true;
clearenv();
internal_status = 0;
}
else if ( strcmp(argv[0], "exec") == 0 )
{
internal = true;
if ( argc == 1 )
{
if ( pipein != 0 )
dup2(pipein, 0);
if ( pipeout != 1 )
dup2(pipeout, 1);
}
else
{
childpid = 0;
argv++;
}
}
else
{
internal = false;
}
if ( !internal && (childpid = fork()) < 0 )
{
error(0, errno, "fork");
internal_status = 1;
failure = true;
internal = true;
childpid = getpid();
}
if ( childpid )
{
if ( set_pipein )
close(pipein);
if ( set_pipeout )
close(pipeout);
for ( size_t i = 0; i < varsc; i++ )
free(varsv[i]);
free(varsv);
for ( size_t i = 0; i < argc; i++ )
free(argv[i]);
free(argv);
if ( internal )
{
struct execute_result result;
memset(&result, 0, sizeof(result));
result.internal_status = internal_status;
result.failure = failure;
result.critical = critical;
result.internal = true;
result.exited = do_exit;
return result;
}
setpgid(childpid, pgid != -1 ? pgid : childpid);
// TODO: This is an inefficient manner to avoid a race condition where
// a pipeline foo | bar is running in its own process group and
// foo is the process group leader that sets itself as the
// foreground process group, but bar dies prior to foo's tcsetpgrp
// call, because then the shell would run tcsetpgrp to take back
// control, and only then would foo do its tcsetpgrp call.
while ( interactive && pgid == -1 && tcgetpgrp(0) != childpid )
sched_yield();
struct execute_result result;
memset(&result, 0, sizeof(result));
result.pid = childpid;
result.internal = false;
return result;
}
setpgid(0, pgid != -1 ? pgid : 0);
if ( interactive && pgid == -1 )
{
sigset_t oldset, sigttou;
sigemptyset(&sigttou);
sigaddset(&sigttou, SIGTTOU);
sigprocmask(SIG_BLOCK, &sigttou, &oldset);
tcsetpgrp(0, getpgid(0));
sigprocmask(SIG_SETMASK, &oldset, NULL);
}
if ( pipein != 0 )
dup2(pipein, 0);
if ( pipeout != 1 )
dup2(pipeout, 1);
for ( size_t i = 0; i < varsc; i++ )
{
char* key = varsv[i];
char* eq = strchr(key, '=');
if ( !eq )
continue;
*eq = '\0';
char* value = eq + 1;
int ret = setenv(key, value, 1);
*eq = '=';
if ( ret < 0 )
{
error(0, errno, "setenv");
status = 1;
}
}
if ( strcmp(argv[0], "history") == 0 )
{
for ( size_t i = 0; i < edit_state.history_used; i++ )
{
const char* line = edit_state.history[i];
if ( fprintf(stdout, "%5zu %s\n", i + 1, line) < 0 )
err(1, "stdout");
}
if ( fflush(stdout) == EOF )
err(1, "stdout");
exit(0);
}
execvp(argv[0], argv);
if ( interactive && errno == ENOENT )
{
int errno_saved = errno;
execlp("command-not-found", "command-not-found", argv[0], (const char*) NULL);
errno = errno_saved;
}
error(127, errno, "%s", argv[0]);
__builtin_unreachable();
}
int run_tokens(char** tokens,
size_t tokens_count,
bool interactive,
bool exit_on_error,
bool* script_exited)
{
size_t cmdnext = 0;
size_t cmdstart;
size_t cmdend;
int pipein = 0;
int pipeout = 1;
int pipeinnext = 0;
const char* execmode;
pid_t pgid = -1;
bool short_circuited_and = false;
bool short_circuited_or = false;
// Collect any pending zombie processes.
while ( 0 < waitpid(-1, NULL, WNOHANG) );
readcmd:
cmdstart = cmdnext;
if ( cmdstart == tokens_count )
return status;
for ( cmdend = cmdstart; cmdend < tokens_count; cmdend++ )
{
const char* token = tokens[cmdend];
if ( strcmp(token, ";") == 0 ||
strcmp(token, "&") == 0 ||
strcmp(token, "&&") == 0 ||
strcmp(token, "|") == 0 ||
strcmp(token, "||") == 0 ||
false )
break;
}
if ( cmdend < tokens_count )
{
execmode = tokens[cmdend];
cmdnext = cmdend + 1;
}
else
{
execmode = ";";
cmdnext = cmdend;
}
if ( short_circuited_or )
{
if ( !strcmp(execmode, ";") ||
!strcmp(execmode, "&") )
{
short_circuited_and = false;
short_circuited_or = false;
}
goto readcmd;
}
if ( short_circuited_and )
{
if ( !strcmp(execmode, ";") ||
!strcmp(execmode, "&") ||
!strcmp(execmode, "||") )
short_circuited_and = false;
goto readcmd;
}
if ( strcmp(execmode, "|") == 0 )
{
int pipes[2];
if ( pipe2(pipes, O_CLOEXEC) < 0 )
{
error(0, errno, "pipe");
if ( !interactive || exit_on_error )
*script_exited = true;
return status = 1;
}
else
{
if ( pipeout != 1 )
close(pipeout);
pipeout = pipes[1];
if ( pipeinnext != 0 )
close(pipeinnext);
pipeinnext = pipes[0];
}
}
struct execute_result result =
execute(tokens + cmdstart,
cmdend - cmdstart,
interactive,
pipein,
pipeout,
pgid);
if ( !result.internal && pgid == -1 )
pgid = result.pid;
if ( pipein != 0 )
{
close(pipein);
pipein = 0;
}
if ( pipeout != 1 )
{
close(pipeout);
pipeout = 1;
}
if ( pipeinnext != 0 )
{
pipein = pipeinnext;
pipeinnext = 0;
}
if ( result.critical )
{
if ( !interactive || exit_on_error )
*script_exited = true;
return status = result.internal_status;
}
if ( result.exited )
{
*script_exited = true;
return status = result.internal_status;
}
if ( strcmp(execmode, "&") == 0 )
{
// TODO: We probably shouldn't have made the progress group foreground
// as that may have side effects if a process checks for this
// behavior and then unexpectedly the shell takes back support
// without the usual ^Z mechanism.
if ( interactive )
{
sigset_t oldset, sigttou;
sigemptyset(&sigttou);
sigaddset(&sigttou, SIGTTOU);
sigprocmask(SIG_BLOCK, &sigttou, &oldset);
tcsetpgrp(0, getpgid(0));
sigprocmask(SIG_SETMASK, &oldset, NULL);
}
pgid = -1;
status = 0;
goto readcmd;
}
if ( strcmp(execmode, "|") == 0 )
goto readcmd;
if ( result.internal )
{
status = result.internal_status;
}
else
{
int exitstatus;
if ( waitpid(result.pid, &exitstatus, 0) < 0 )
{
error(0, errno, "waitpid");
if ( !interactive || exit_on_error )
*script_exited = true;
status = 1;
return status;
}
if ( interactive )
{
sigset_t oldset, sigttou;
sigemptyset(&sigttou);
sigaddset(&sigttou, SIGTTOU);
sigprocmask(SIG_BLOCK, &sigttou, &oldset);
tcsetpgrp(0, getpgid(0));
sigprocmask(SIG_SETMASK, &oldset, NULL);
}
if ( WIFSIGNALED(exitstatus) && WTERMSIG(exitstatus) == SIGINT )
printf("^C\n");
else if ( WIFSIGNALED(exitstatus) && WTERMSIG(exitstatus) != SIGPIPE )
printf("%s\n", strsignal(WTERMSIG(exitstatus)));
status = WEXITSTATUS(exitstatus);
}
pgid = -1;
if ( !strcmp(execmode, "&&") )
{
if ( status != 0 )
short_circuited_and = true;
}
else if ( !strcmp(execmode, "||") )
{
if ( status == 0 )
short_circuited_or = true;
}
else if ( exit_on_error && status != 0 )
{
*script_exited = true;
return status;
}
goto readcmd;
}
int run_command(char* command,
bool interactive,
bool exit_on_error,
bool* script_exited)
{
int result;
char** tokens = NULL;
size_t tokens_used = 0;
size_t tokens_length = 0;
enum sh_tokenize_result tokenize_result =
sh_tokenize(command, &tokens, &tokens_used, &tokens_length);
if ( tokenize_result == SH_TOKENIZE_RESULT_OK )
{
result = run_tokens(tokens, tokens_used, interactive, exit_on_error,
script_exited);
}
else
{
if ( !interactive )
*script_exited = true;
result = 255;
}
for ( size_t i = 0; i < tokens_used; i++ )
free(tokens[i]);
free(tokens);
return result;
}
bool does_line_editing_need_another_line(void* ctx, const char* line)
{
(void) ctx;
return !is_shell_input_ready(line);
}
bool is_outermost_shell(void)
{
char* name = ttyname(0);
if ( !name || strcmp(name, "/dev/tty1") != 0 )
return false;
const char* shlvl_str = getenv("SHLVL");
if ( !shlvl_str )
return true;
return atol(shlvl_str) <= 1;
}
void on_trap_eof(void* edit_state_ptr)
{
if ( is_outermost_shell() )
return;
struct edit_line* edit_state = (struct edit_line*) edit_state_ptr;
edit_line_type_codepoint(edit_state, L'e');
edit_line_type_codepoint(edit_state, L'x');
edit_line_type_codepoint(edit_state, L'i');
edit_line_type_codepoint(edit_state, L't');
}
bool is_usual_char_for_completion(char c)
{
return !isspace((unsigned char) c) &&
c != ';' && c != '&' && c != '|' &&
c != '<' && c != '>' && c != '#' && c != '$';
}
size_t do_complete(char*** completions_ptr,
size_t* used_before_ptr,
size_t* used_after_ptr,
void* ctx,
const char* partial,
size_t complete_at)
{
(void) ctx;
size_t used_before = 0;
size_t used_after = 0;
while ( complete_at - used_before &&
is_usual_char_for_completion(partial[complete_at - (used_before+1)]) )
used_before++;
#if 0
while ( partial[complete_at + used_after] &&
is_usual_char_for_completion(partial[complete_at + used_after]) )
used_after++;
#endif
enum complete_type
{
COMPLETE_TYPE_FILE,
COMPLETE_TYPE_EXECUTABLE,
COMPLETE_TYPE_DIRECTORY,
COMPLETE_TYPE_PROGRAM,
COMPLETE_TYPE_VARIABLE,
};
enum complete_type complete_type = COMPLETE_TYPE_FILE;
if ( complete_at - used_before && partial[complete_at - used_before-1] == '$' )
{
complete_type = COMPLETE_TYPE_VARIABLE;
used_before++;
}
else
{
size_t type_offset = complete_at - used_before;
while ( type_offset && isspace((unsigned char) partial[type_offset-1]) )
type_offset--;
if ( 2 <= type_offset &&
strncmp(partial + type_offset - 2, "cd", 2) == 0 &&
(type_offset == 2 || !is_usual_char_for_completion(partial[type_offset-2-1])) )
complete_type = COMPLETE_TYPE_DIRECTORY;
else if ( !type_offset ||
partial[type_offset-1] == ';' ||
partial[type_offset-1] == '&' ||
partial[type_offset-1] == '|' )
{
if ( memchr(partial + complete_at - used_before, '/', used_before) )
complete_type = COMPLETE_TYPE_EXECUTABLE;
else
complete_type = COMPLETE_TYPE_PROGRAM;
}
}
char** completions = NULL;
size_t completions_count = 0;
size_t completions_length = 0;
if ( complete_type == COMPLETE_TYPE_PROGRAM ) do
{
for ( size_t i = 0; builtin_commands[i]; i++ )
{
const char* builtin = builtin_commands[i];
if ( strncmp(builtin, partial + complete_at - used_before, used_before) != 0 )
continue;
// TODO: Add allocation check!
array_add((void***) &completions,
&completions_count,
&completions_length,
strdup(builtin + used_before));
}
char* path = strdup_safe(getenv("PATH"));
if ( !path )
{
complete_type = COMPLETE_TYPE_FILE;
break;
}
char* path_input = path;
char* component;
while ( (component = strsep(&path_input, ":")) )
{
DIR* dir;
if ( (dir = opendir(component)) )
{
struct dirent* entry;
while ( (entry = readdir(dir)) )
{
if ( strncmp(entry->d_name, partial + complete_at - used_before, used_before) != 0 )
continue;
if ( used_before == 0 && entry->d_name[0] == '.' )
continue;
// TODO: Add allocation check!
array_add((void***) &completions,
&completions_count,
&completions_length,
strdup(entry->d_name + used_before));
}
closedir(dir);
}
}
free(path);
} while ( false );
if ( complete_type == COMPLETE_TYPE_FILE ||
complete_type == COMPLETE_TYPE_EXECUTABLE ||
complete_type == COMPLETE_TYPE_DIRECTORY ) do
{
const char* pattern = partial + complete_at - used_before;
size_t pattern_length = used_before;
char* dirpath_alloc = NULL;
const char* dirpath;
if ( !memchr(pattern, '/', pattern_length) )
dirpath = ".";
else if ( pattern_length && pattern[pattern_length-1] == '/' )
{
dirpath_alloc = strndup(pattern, pattern_length);
if ( !dirpath_alloc )
break;
dirpath = dirpath_alloc;
pattern += pattern_length;
pattern_length = 0;
}
else
{
dirpath_alloc = strndup(pattern, pattern_length);
if ( !dirpath_alloc )
break;
dirpath = dirname(dirpath_alloc);
const char* last_slash = (const char*) memrchr(pattern, '/', pattern_length);
size_t last_slash_offset = (uintptr_t) last_slash - (uintptr_t) pattern;
pattern += last_slash_offset + 1;
pattern_length -= last_slash_offset + 1;
}
used_before = pattern_length;
DIR* dir = opendir(dirpath);
if ( !dir )
{
free(dirpath_alloc);
break;
}
struct dirent* entry;
while ( (entry = readdir(dir)) )
{
if ( strncmp(entry->d_name, pattern, pattern_length) != 0 )
continue;
if ( pattern_length == 0 && entry->d_name[0] == '.' )
continue;
struct stat st;
bool is_directory = entry->d_type == DT_DIR ||
(entry->d_type == DT_UNKNOWN &&
!fstatat(dirfd(dir), entry->d_name, &st, 0) &&
S_ISDIR(st.st_mode));
bool is_executable = complete_type == COMPLETE_TYPE_EXECUTABLE &&
!fstatat(dirfd(dir), entry->d_name, &st, 0) &&
st.st_mode & 0111;
if ( complete_type == COMPLETE_TYPE_DIRECTORY && !is_directory )
continue;
if ( complete_type == COMPLETE_TYPE_EXECUTABLE &&
!(is_directory || is_executable) )
continue;
size_t name_length = strlen(entry->d_name);
char* completion = (char*) malloc(name_length - pattern_length + 1 + 1);
if ( !completion )
continue;
strcpy(completion, entry->d_name + pattern_length);
if ( is_directory )
strcat(completion, "/");
// TODO: Add allocation check!
array_add((void***) &completions,
&completions_count,
&completions_length,
completion);
}
closedir(dir);
free(dirpath_alloc);
} while ( false );
if ( complete_type == COMPLETE_TYPE_VARIABLE ) do
{
const char* pattern = partial + complete_at - used_before + 1;
size_t pattern_length = used_before - 1;
if ( memchr(pattern, '=', pattern_length) )
break;
for ( size_t i = 0; environ[i]; i++ )
{
if ( strncmp(pattern, environ[i], pattern_length) != 0 )
continue;
const char* rest = environ[i] + pattern_length;
size_t equal_offset = strcspn(rest, "=");
if ( rest[equal_offset] != '=' )
continue;
// TODO: Add allocation check!
array_add((void***) &completions,
&completions_count,
&completions_length,
strndup(rest, equal_offset));
}
} while ( false );
*used_before_ptr = used_before;
*used_after_ptr = used_after;
return *completions_ptr = completions, completions_count;
}
static void eval_ps_append_c(struct stringbuf* buf, char c)
{
if ( c == '\\' || c == '\'' || c == '"' || c == '$' || c == '`' )
stringbuf_append_c(buf, '\\');
stringbuf_append_c(buf, c);
}
static void eval_ps_append(struct stringbuf* buf, const char* str)
{
for ( size_t i = 0; str[i]; i++ )
eval_ps_append_c(buf, str[i]);
}
static char* eval_ps(const char* ps)
{
struct stringbuf buf;
stringbuf_begin(&buf);
bool escaped = false;
while ( *ps )
{
char c = *ps++;
if ( !escaped && c == '\\' )
{
escaped = true;
continue;
}
else if ( escaped && '0' <= c && c <= '7' )
{
unsigned char byte = c - '0';
if ( '0' <= *ps && *ps <= '7' )
{
byte = byte * 8 + *ps++ - '0';
if ( byte <= 037 && '0' <= *ps && *ps <= '7' )
byte = byte * 8 + *ps++ - '0';
}
eval_ps_append_c(&buf, byte);
}
else if ( escaped && c == 'a' )
eval_ps_append_c(&buf, '\a');
else if ( escaped && c == 'e' )
eval_ps_append_c(&buf, '\e');
else if ( escaped && (c == 'h' || c == 'H') )
{
char hostname[HOST_NAME_MAX + 1] = "?";
gethostname(hostname, sizeof(hostname));
if ( c == 'h' )
hostname[strcspn(hostname, ".")] = '\0';
eval_ps_append(&buf, hostname);
}
else if ( escaped && c == 'l' )
{
char* tty = ttyname(0);
if ( tty )
eval_ps_append(&buf, basename(tty));
else
eval_ps_append_c(&buf, '?');
}
else if ( escaped && c == 'n' )
eval_ps_append_c(&buf, '\n');
else if ( escaped && c == 'r' )
eval_ps_append_c(&buf, '\r');
else if ( escaped && c == 's' )
{
const char* argv0 = getenv("0");
if ( !argv0 )
argv0 = program_invocation_short_name;
char* base = strdup(argv0);
if ( !base )
eval_ps_append_c(&buf, '?');
else
{
eval_ps_append(&buf, basename(base));
free(base);
}
}
else if ( escaped && (c == 't' || c == 'T' || c == '@' || c == 'A') )
{
const char* format = "";
switch ( c )
{
case 't': format = "%H:%M:%S"; break;
case 'T': format = "%I:%M:%S"; break;
case '@': format = "%I:%M %p"; break;
case 'A': format = "%H:%M"; break;
}
time_t now = time(NULL);
struct tm tm;
localtime_r(&now, &tm);
char buffer[16] = "";
strftime(buffer, sizeof(buffer), format, &tm);
eval_ps_append(&buf, buffer);
}
else if ( escaped && c == 'u' )
{
char* user = getlogin();
eval_ps_append(&buf, user ? user : "?");
}
else if ( escaped && (c == 'w' || c == 'W') )
{
char* dir = get_current_dir_name();
const char* home = getenv("HOME");
if ( !dir )
eval_ps_append_c(&buf, '?');
else if ( c == 'w' )
{
size_t home_len = home ? strlen(home) : 0;
if ( home_len && !strncmp(dir, home, home_len) )
{
eval_ps_append_c(&buf, '~');
eval_ps_append(&buf, dir + home_len);
}
else
eval_ps_append(&buf, dir);
}
else if ( home && !strcmp(dir, home) )
eval_ps_append_c(&buf, '~');
else
eval_ps_append(&buf, basename(dir));
free(dir);
}
else if ( escaped && c == '$' )
eval_ps_append_c(&buf, getuid() == 0 ? '#' : '$');
else if ( escaped && (c == '[' || c == ']') )
{
// TODO: Ignoring this sequence when predicting cursor position.
}
else
{
if ( escaped || c == '\'' || c == '"' )
stringbuf_append_c(&buf, '\\');
stringbuf_append_c(&buf, c);
}
escaped = false;
}
char* string = stringbuf_finish(&buf);
if ( !string )
return NULL;
char* expanded = token_expand_variables(string);
free(string);
if ( !expanded )
return NULL;
char* finalized = token_finalize(expanded);
free(expanded);
return finalized;
}
struct sh_read_command
{
char* command;
bool abort_condition;
bool eof_condition;
bool error_condition;
};
void read_command_interactive(struct sh_read_command* sh_read_command)
{
update_env();
edit_state.in_fd = 0;
edit_state.out_fd = 1;
edit_state.check_input_incomplete_context = NULL;
edit_state.check_input_incomplete = does_line_editing_need_another_line;
edit_state.trap_eof_opportunity_context = &edit_state;
edit_state.trap_eof_opportunity = on_trap_eof;
edit_state.complete_context = NULL;
edit_state.complete = do_complete;
const char* def_ps1 = !getuid() ?
"\\033[;1;31m\\u\033[1;33m@\\H \\033[1;34m\\w \\033[1;31m\\$\\033[m " :
"\\033[;1;32m\\u@\\H \\033[1;34m\\w \\$\\033[m ";
const char* def_ps2 = "> ";
edit_state.ps1 = eval_ps(getenv_safe_def("PS1", def_ps1));
edit_state.ps2 = eval_ps(getenv_safe_def("PS2", def_ps2));
edit_line(&edit_state);
free((char*) edit_state.ps1);
free((char*) edit_state.ps2);
if ( edit_state.abort_editing )
{
sh_read_command->abort_condition = true;
return;
}
if ( edit_state.eof_condition )
{
sh_read_command->eof_condition = true;
return;
}
char* command = edit_line_result(&edit_state);
assert(command);
sh_read_command->command = command;
}
void read_command_non_interactive(struct sh_read_command* sh_read_command,
FILE* fp)
{
int fd = fileno(fp);
size_t command_used = 0;
size_t command_length = 1024;
char* command = (char*) malloc(command_length + 1);
if ( !command )
error(64, errno, "malloc");
command[0] = '\0';
while ( true )
{
char c;
if ( 0 <= fd )
{
ssize_t bytes_read = read(fd, &c, sizeof(c));
if ( bytes_read < 0 )
{
sh_read_command->error_condition = true;
free(command);
return;
}
else if ( bytes_read == 0 )
{
if ( command_used == 0 )
{
sh_read_command->eof_condition = true;
free(command);
return;
}
else
{
c = '\n';
}
}
else
{
assert(bytes_read == 1);
if ( c == '\0' )
continue;
}
}
else
{
int ic = fgetc(fp);
if ( ic == EOF && ferror(fp) )
{
sh_read_command->error_condition = true;
free(command);
return;
}
else if ( ic == EOF )
{
if ( command_used == 0 )
{
sh_read_command->eof_condition = true;
free(command);
return;
}
else
{
c = '\n';
}
}
else
{
c = (char) (unsigned char) ic;
if ( c == '\0' )
continue;
}
}
if ( c == '\n' && is_shell_input_ready(command) )
break;
if ( command_used == command_length )
{
size_t new_length = command_length * 2;
char* new_command = (char*) realloc(command, new_length + 1);
if ( !new_command )
error(64, errno, "realloc");
command = new_command;
command_length = new_length;
}
command[command_used++] = c;
command[command_used] = '\0';
}
sh_read_command->command = command;
}
static int run(FILE* fp,
const char* fp_name,
bool interactive,
bool exit_on_error,
bool* script_exited,
int status)
{
// TODO: The interactive read code should cope when the input is not a
// terminal; it should print the prompt and then read normally without
// any line editing features.
if ( !isatty(fileno(fp)) || !foreground_shell )
interactive = false;
while ( true )
{
struct sh_read_command sh_read_command;
memset(&sh_read_command, 0, sizeof(sh_read_command));
if ( interactive )
read_command_interactive(&sh_read_command);
else
read_command_non_interactive(&sh_read_command, fp);
if ( sh_read_command.abort_condition )
continue;
if ( sh_read_command.eof_condition )
{
if ( interactive && is_outermost_shell() )
{
printf("Type exit to close the outermost shell.\n");
continue;
}
break;
}
if ( sh_read_command.error_condition )
{
error(0, errno, "read: %s", fp_name);
return *script_exited = true, 2;
}
status = run_command(sh_read_command.command, interactive,
exit_on_error, script_exited);
free(sh_read_command.command);
if ( *script_exited || (status == 0 && exit_on_error) )
break;
}
return status;
}
static char* find_rc(bool login)
{
const char* env = getenv("ENV");
if ( !login && env )
{
// TODO: Path expansion.
char* result = strdup(env);
if ( !result )
err(1, "malloc");
return result;
}
const char* home = getenv("HOME");
const char* rcname = login ? "profile" : "shrc";
const char* dirs[] = { home, "/etc", "/etc/default" };
bool found = false;
for ( size_t i = 0; !found && i < sizeof(dirs) / sizeof(dirs[0]); i++ )
{
char* rc;
if ( asprintf(&rc, "%s%s%s", dirs[i], i ? "/" : "/.", rcname) < 0 )
err(1, "malloc");
if ( (found = !access(rc, F_OK)) )
return rc;
}
return NULL;
}
static int top(FILE* fp,
const char* fp_name,
bool interactive,
bool exit_on_error,
bool login,
bool* script_exited,
int status)
{
if ( interactive )
{
const char* home = getenv("HOME");
const char* histfile = getenv("HISTFILE");
if ( !histfile && home )
{
char* path;
if ( asprintf(&path, "%s/.sh_history", home) < 0 ||
setenv("HISTFILE", path, 1) < 0 )
err(1, "malloc");
free(path);
}
char* rc = find_rc(login);
if ( rc )
{
FILE* rcfp = fopen(rc, "r");
if ( !rcfp )
warn("%s", rc);
else
{
status = run(rcfp, rc, false, exit_on_error, script_exited,
status);
fclose(rcfp);
}
free(rc);
}
if ( *script_exited || (status != 0 && exit_on_error) )
return status;
edit_line_history_load(&edit_state, getenv("HISTFILE"));
}
status = run(fp, fp_name, interactive, exit_on_error, script_exited,
status);
if ( interactive )
edit_line_history_save(&edit_state, getenv("HISTFILE"));
return status;
}
static void compact_arguments(int* argc, char*** argv)
{
for ( int i = 0; i < *argc; i++ )
{
while ( i < *argc && !(*argv)[i] )
{
for ( int n = i; n < *argc; n++ )
(*argv)[n] = (*argv)[n+1];
(*argc)--;
}
}
}
static void help(FILE* fp, const char* argv0)
{
fprintf(fp, "Usage: %s [OPTION...] [SCRIPT [ARGUMENT...]]\n", argv0);
fprintf(fp, " or: %s [OPTION...] -c COMMAND [ARGUMENT...]\n", argv0);
fprintf(fp, " or: %s [OPTION...] -s [ARGUMENT...]\n", argv0);
#if 0
fprintf(fp, " -a, +a set -a\n");
fprintf(fp, " -b, +b set -b\n");
#endif
fprintf(fp, " -c execute the first operand as the command\n");
#if 0
fprintf(fp, " -C, +C set -C\n");
fprintf(fp, " -e, +e set -e\n");
fprintf(fp, " -f, +f set -f\n");
fprintf(fp, " -h, +h set -h\n");
#endif
fprintf(fp, " -i shell is interactive\n");
#if 0
fprintf(fp, " -m, +m set -m\n");
fprintf(fp, " -n, +n set -n\n");
fprintf(fp, " -o OPTION set -o OPTION\n");
fprintf(fp, " +o OPTION set +o OPTION\n");
#endif
fprintf(fp, " -s read commands from the standard input\n");
#if 0
fprintf(fp, " -u, +u set -u\n");
fprintf(fp, " -v, +v set -v\n");
fprintf(fp, " -x, +x set -x\n");
#endif
fprintf(fp, " --help display this help and exit\n");
fprintf(fp, " --version output version information and exit\n");
}
static void version(FILE* fp, const char* argv0)
{
fprintf(fp, "%s (Sortix) %s\n", argv0, VERSIONSTR);
}
int main(int argc, char* argv[])
{
setlocale(LC_ALL, "");
foreground_shell = isatty(0) && tcgetpgrp(0) == getpgid(0);
// TODO: Canonicalize argv[0] if it contains a slash and isn't absolute?
const char* env_pwd;
if ( (env_pwd = getenv("PWD")) )
{
if ( !is_proper_absolute_path(env_pwd) )
{
unsetenv("PWD");
char* real_pwd = get_current_dir_name();
if ( real_pwd )
setenv("PWD", real_pwd, 1);
free(real_pwd);
}
}
bool flag_c_first_operand_is_command = false;
bool flag_e_exit_on_error = false;
bool flag_i_interactive = false;
bool flag_l_login = argv[0][0] == '-';
bool flag_s_stdin = false;
// The well implemented options are recognized in proper-sh.c.
const char* argv0 = argv[0];
for ( int i = 1; i < argc; i++ )
{
const char* arg = argv[i];
if ( (arg[0] != '-' && arg[0] != '+') || !arg[1] )
break; // Intentionally not continue and note '+' support.
argv[i] = NULL;
if ( !strcmp(arg, "--") )
break;
if ( arg[0] == '+' )
{
char c;
while ( (c = *++arg) ) switch ( c )
{
case 'c': flag_c_first_operand_is_command = false; break;
case 'e': flag_e_exit_on_error = false; break;
case 'i': flag_i_interactive = false; break;
case 'l': flag_l_login = false; break;
case 's': flag_s_stdin = false; break;
default:
fprintf(stderr, "%s: unknown option -- '%c'\n", argv0, c);
help(stderr, argv0);
exit(1);
}
}
else if ( arg[1] != '-' )
{
char c;
while ( (c = *++arg) ) switch ( c )
{
case 'c': flag_c_first_operand_is_command = true; break;
case 'e': flag_e_exit_on_error = true; break;
case 'i': flag_i_interactive = true; break;
case 'l': flag_l_login = true; break;
case 's': flag_s_stdin = true; break;
default:
fprintf(stderr, "%s: unknown option -- '%c'\n", argv0, c);
help(stderr, argv0);
exit(1);
}
}
else if ( !strcmp(arg, "--help") )
help(stdout, argv0), exit(0);
else if ( !strcmp(arg, "--version") )
version(stdout, argv0), exit(0);
else
{
fprintf(stderr, "%s: unknown option: %s\n", argv0, arg);
help(stderr, argv0);
exit(1);
}
}
compact_arguments(&argc, &argv);
if ( getenv("SHLVL") )
{
long shlvl = atol(getenv("SHLVL"));
if ( shlvl < 1 )
shlvl = 1;
else if ( shlvl < LONG_MAX )
shlvl++;
char shlvl_string[sizeof(long) * 3];
snprintf(shlvl_string, sizeof(shlvl_string), "%li", shlvl);
setenv("SHLVL", shlvl_string, 1);
}
else
{
setenv("SHLVL", "1", 1);
}
char pidstr[3 * sizeof(pid_t)];
char ppidstr[3 * sizeof(pid_t)];
snprintf(pidstr, sizeof(pidstr), "%ji", (intmax_t) getpid());
snprintf(ppidstr, sizeof(ppidstr), "%ji", (intmax_t) getppid());
setenv("SHELL", argv[0] + (argv[0][0] == '-'), 1);
setenv("$", pidstr, 1);
setenv("PPID", ppidstr, 1);
setenv("?", "0", 1);
setenv("0", argv[0], 1);
bool script_exited = false;
int status = 0;
if ( flag_c_first_operand_is_command )
{
if ( argc <= 1 )
error(2, 0, "option -c expects an operand");
for ( int i = 2; i < argc; i++ )
{
char varname[sizeof(int) * 3];
snprintf(varname, sizeof(varname), "%i", i - 2);
setenv(varname, argv[i], 1);
}
const char* command = argv[1];
size_t command_length = strlen(command);
FILE* fp = fmemopen((void*) command, command_length, "r");
if ( !fp )
error(2, errno, "fmemopen");
status = top(fp, "<command-line>", false, flag_e_exit_on_error,
flag_l_login, &script_exited, status);
fclose(fp);
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
exit(status);
if ( flag_s_stdin )
{
bool is_interactive = flag_i_interactive || isatty(fileno(stdin));
status = top(stdin, "<stdin>", is_interactive, flag_e_exit_on_error,
flag_l_login, &script_exited, status);
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
exit(status);
}
}
else if ( flag_s_stdin )
{
for ( int i = 1; i < argc; i++ )
{
char varname[sizeof(int) * 3];
snprintf(varname, sizeof(varname), "%i", i - 1);
setenv(varname, argv[i], 1);
}
bool is_interactive = flag_i_interactive || isatty(fileno(stdin));
status = top(stdin, "<stdin>", is_interactive, flag_e_exit_on_error,
flag_l_login, &script_exited, status);
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
exit(status);
}
else if ( 2 <= argc )
{
for ( int i = 1; i < argc; i++ )
{
char varname[sizeof(int) * 3];
snprintf(varname, sizeof(varname), "%i", i - 1);
setenv(varname, argv[i], 1);
}
const char* path = argv[1];
FILE* fp = fopen(path, "r");
if ( !fp )
error(127, errno, "%s", path);
status = top(fp, path, false, flag_e_exit_on_error, flag_l_login,
&script_exited, status);
fclose(fp);
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
exit(status);
}
else
{
bool is_interactive = flag_i_interactive || isatty(fileno(stdin));
status = top(stdin, "<stdin>", is_interactive, flag_e_exit_on_error,
flag_l_login, &script_exited, status);
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
exit(status);
}
return 0;
}