Shell: Implement redirections

This works well enough. At the moment redirections can appear anywhere
in the command **after** environment variables and possible alias
command. This means commands like `>foo echo hello` are not supported.
I don't think this is big enough problem for now (99% time redirections
are given after arguments) that i would rewrite the environment and
alias parsing.
This commit is contained in:
Bananymous 2025-07-22 16:54:06 +03:00
parent 05affda20c
commit c1978f9133
8 changed files with 212 additions and 9 deletions

View File

@ -2,7 +2,7 @@
#include <BAN/String.h>
#define COMMAND_GET_MACRO(_0, _1, _2, NAME, ...) NAME
#define COMMAND_GET_MACRO(_0, _1, _2, _3, NAME, ...) NAME
#define COMMAND_MOVE_0(class) \
class(class&& o) { } \
@ -13,7 +13,10 @@
#define COMMAND_MOVE_2(class, var1, var2) \
class(class&& o) { var1 = BAN::move(o.var1); var2 = BAN::move(o.var2); } \
class& operator=(class&& o) { var1 = BAN::move(o.var1); var2 = BAN::move(o.var2); return *this; }
#define COMMAND_MOVE(class, ...) COMMAND_GET_MACRO(_0 __VA_OPT__(,) __VA_ARGS__, COMMAND_MOVE_2, COMMAND_MOVE_1, COMMAND_MOVE_0)(class, __VA_ARGS__)
#define COMMAND_MOVE_3(class, var1, var2, var3) \
class(class&& o) { var1 = BAN::move(o.var1); var2 = BAN::move(o.var2); var3 = BAN::move(o.var3); } \
class& operator=(class&& o) { var1 = BAN::move(o.var1); var2 = BAN::move(o.var2); var3 = BAN::move(o.var3); return *this; }
#define COMMAND_MOVE(class, ...) COMMAND_GET_MACRO(_0 __VA_OPT__(,) __VA_ARGS__, COMMAND_MOVE_3, COMMAND_MOVE_2, COMMAND_MOVE_1, COMMAND_MOVE_0)(class, __VA_ARGS__)
#define COMMAND_RULE5(class, ...) \
class() = default; \
@ -67,9 +70,19 @@ struct SingleCommand
CommandArgument value;
};
COMMAND_RULE5(SingleCommand, environment, arguments);
struct Redirection
{
CommandArgument destination;
int source_fd;
bool append;
bool duplicate;
bool input;
};
COMMAND_RULE5(SingleCommand, environment, arguments, redirections);
BAN::Vector<EnvironmentVariable> environment;
BAN::Vector<CommandArgument> arguments;
BAN::Vector<Redirection> redirections;
};
struct PipedCommand

View File

@ -99,6 +99,47 @@ BAN::ErrorOr<Execute::ExecuteResult> Execute::execute_command_no_wait(const Inte
CHECK_FD_OR_PERROR_AND_EXIT(command.fd_in, STDIN_FILENO);
CHECK_FD_OR_PERROR_AND_EXIT(command.fd_out, STDOUT_FILENO);
for (const auto& redirection : command.redirections)
{
int dst_fd = -1;
if (redirection.duplicate)
{
if (!redirection.path.empty())
{
dst_fd = 0;
for (char ch : redirection.path)
{
if (!isdigit(ch))
{
dst_fd = -1;
break;
}
dst_fd = (dst_fd * 10) + (ch - '0');
}
}
}
else
{
const int flags = O_CREAT
| (redirection.input ? O_RDONLY : O_WRONLY)
| (redirection.append ? O_APPEND : O_TRUNC);
dst_fd = open(redirection.path.data(), flags, 0644);
if (dst_fd == -1)
{
perror("open");
exit(errno);
}
}
CHECK_FD_OR_PERROR_AND_EXIT(dst_fd, redirection.source_fd);
if (!redirection.duplicate)
close(dst_fd);
}
execv(command.command.get<BAN::String>().data(), const_cast<char* const*>(exec_args.data()));
perror("execv");
exit(errno);
@ -125,6 +166,7 @@ BAN::ErrorOr<int> Execute::execute_command_sync(BAN::Span<const BAN::String> arg
.command = {},
.arguments = arguments,
.environments = {},
.redirections = {},
.fd_in = fd_in,
.fd_out = fd_out,
.background = false,
@ -194,6 +236,22 @@ BAN::ErrorOr<void> Execute::execute_command(const PipedCommand& piped_command)
return result;
};
const auto evaluate_redirections =
[this](BAN::Span<const SingleCommand::Redirection> redirections) -> BAN::ErrorOr<BAN::Vector<InternalCommand::Redirection>>
{
BAN::Vector<InternalCommand::Redirection> result;
TRY(result.reserve(redirections.size()));
for (const auto& redirection : redirections)
TRY(result.push_back({
.path = TRY(redirection.destination.evaluate(*this)),
.source_fd = redirection.source_fd,
.append = redirection.append,
.duplicate = redirection.duplicate,
.input = redirection.input,
}));
return result;
};
const int stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
if (stdin_flags == -1)
perror("fcntl");
@ -221,11 +279,13 @@ BAN::ErrorOr<void> Execute::execute_command(const PipedCommand& piped_command)
const auto arguments = TRY_OR_PERROR_AND_BREAK(evaluate_arguments(piped_command.commands[i].arguments.span()));
const auto environments = TRY_OR_PERROR_AND_BREAK(evaluate_environment(piped_command.commands[i].environment.span()));
const auto redirections = TRY_OR_PERROR_AND_BREAK(evaluate_redirections(piped_command.commands[i].redirections.span()));
InternalCommand command {
.command = {},
.arguments = arguments.span(),
.environments = environments.span(),
.redirections = redirections.span(),
.fd_in = fd_in,
.fd_out = fd_out,
.background = piped_command.background,

View File

@ -12,6 +12,7 @@ class Execute
public:
Execute() = default;
// FIXME: remove this, this is only used by *builtin* time which should not be a builtin
BAN::ErrorOr<int> execute_command_sync(BAN::Span<const BAN::String> arguments, int fd_in, int fd_out);
BAN::ErrorOr<void> execute_command(const SingleCommand&, int fd_in, int fd_out, bool background, pid_t pgrp = 0);
BAN::ErrorOr<void> execute_command(const PipedCommand&);
@ -39,9 +40,19 @@ private:
BAN::String value;
};
struct Redirection
{
BAN::String path;
int source_fd;
bool append;
bool duplicate;
bool input;
};
Command command;
BAN::Span<const BAN::String> arguments;
BAN::Span<const Environment> environments;
BAN::Span<Redirection> redirections;
int fd_in;
int fd_out;
bool background;

View File

@ -21,6 +21,8 @@ BAN::ErrorOr<BAN::Vector<Token>> tokenize_string(BAN::StringView string)
case ')': return Token::Type::CloseParen;
case '$': return Token::Type::Dollar;
case '"': return Token::Type::DoubleQuote;
case '>': return Token::Type::GreaterThan;
case '<': return Token::Type::LessThan;
case '{': return Token::Type::OpenCurly;
case '(': return Token::Type::OpenParen;
case '|': return Token::Type::Pipe;

View File

@ -27,6 +27,12 @@ void Token::debug_dump() const
case Type::DoubleQuote:
dprintln("Token <DoubleQuote>");
break;
case Type::GreaterThan:
dprintln("Token <GreaterThan>");
break;
case Type::LessThan:
dprintln("Token <LessThan>");
break;
case Type::OpenCurly:
dprintln("Token <OpenCurly>");
break;

View File

@ -19,6 +19,8 @@ public:
CloseParen,
Dollar,
DoubleQuote,
GreaterThan,
LessThan,
OpenCurly,
OpenParen,
Pipe,

View File

@ -7,6 +7,7 @@
#include <BAN/ScopeGuard.h>
#include <stdio.h>
#include <unistd.h>
static constexpr bool can_parse_argument_from_token_type(Token::Type token_type)
{
@ -18,6 +19,8 @@ static constexpr bool can_parse_argument_from_token_type(Token::Type token_type)
case Token::Type::Ampersand:
case Token::Type::CloseCurly:
case Token::Type::CloseParen:
case Token::Type::GreaterThan:
case Token::Type::LessThan:
case Token::Type::OpenCurly:
case Token::Type::OpenParen:
case Token::Type::Pipe:
@ -43,6 +46,8 @@ static constexpr char token_type_to_single_character(Token::Type type)
case Token::Type::CloseParen: return ')';
case Token::Type::Dollar: return '$';
case Token::Type::DoubleQuote: return '"';
case Token::Type::GreaterThan: return '>';
case Token::Type::LessThan: return '<';
case Token::Type::OpenCurly: return '{';
case Token::Type::OpenParen: return '(';
case Token::Type::Pipe: return '|';
@ -74,6 +79,10 @@ static constexpr BAN::Error unexpected_token_error(Token::Type type)
return BAN::Error::from_literal("unexpected token $");
case Token::Type::DoubleQuote:
return BAN::Error::from_literal("unexpected token \"");
case Token::Type::GreaterThan:
return BAN::Error::from_literal("unexpected token >");
case Token::Type::LessThan:
return BAN::Error::from_literal("unexpected token <");
case Token::Type::OpenCurly:
return BAN::Error::from_literal("unexpected token {");
case Token::Type::Pipe:
@ -185,6 +194,8 @@ BAN::ErrorOr<CommandArgument::ArgumentPart> TokenParser::parse_backslash(bool is
case Token::Type::CloseParen:
case Token::Type::Dollar:
case Token::Type::DoubleQuote:
case Token::Type::GreaterThan:
case Token::Type::LessThan:
case Token::Type::OpenCurly:
case Token::Type::OpenParen:
case Token::Type::Pipe:
@ -277,6 +288,8 @@ BAN::ErrorOr<CommandArgument::ArgumentPart> TokenParser::parse_dollar()
case Token::Type::CloseCurly:
case Token::Type::CloseParen:
case Token::Type::DoubleQuote:
case Token::Type::GreaterThan:
case Token::Type::LessThan:
case Token::Type::Pipe:
case Token::Type::Semicolon:
case Token::Type::SingleQuote:
@ -363,6 +376,8 @@ BAN::ErrorOr<CommandArgument::ArgumentPart> TokenParser::parse_single_quote()
case Token::Type::CloseParen:
case Token::Type::Dollar:
case Token::Type::DoubleQuote:
case Token::Type::GreaterThan:
case Token::Type::LessThan:
case Token::Type::OpenCurly:
case Token::Type::OpenParen:
case Token::Type::Pipe:
@ -403,6 +418,8 @@ BAN::ErrorOr<CommandArgument> TokenParser::parse_argument()
case Token::Type::Ampersand:
case Token::Type::CloseCurly:
case Token::Type::CloseParen:
case Token::Type::GreaterThan:
case Token::Type::LessThan:
case Token::Type::OpenCurly:
case Token::Type::OpenParen:
case Token::Type::Pipe:
@ -466,6 +483,56 @@ BAN::ErrorOr<CommandArgument> TokenParser::parse_argument()
return result;
}
BAN::ErrorOr<SingleCommand::Redirection> TokenParser::parse_redirection()
{
int source_fd = -1;
if (peek_token().type() == Token::Type::String)
{
const auto string = read_token().string();
source_fd = 0;
for (char ch : string)
source_fd = (source_fd * 10) + (ch - '0');
}
const auto token_type = peek_token().type();
consume_token();
switch (token_type)
{
case Token::Type::GreaterThan:
if (source_fd == -1)
source_fd = STDOUT_FILENO;
break;
case Token::Type::LessThan:
if (source_fd == -1)
source_fd = STDIN_FILENO;
break;
default:
ASSERT_NOT_REACHED();
}
const bool append = (peek_token().type() == token_type);
if (append)
consume_token();
const bool duplicate = (peek_token().type() == Token::Type::Ampersand);
if (duplicate)
consume_token();
while (peek_token().type() == Token::Type::Whitespace)
consume_token();
return SingleCommand::Redirection {
.destination = TRY(parse_argument()),
.source_fd = source_fd,
.append = append,
.duplicate = duplicate,
.input = (token_type == Token::Type::LessThan),
};
}
BAN::ErrorOr<SingleCommand> TokenParser::parse_single_command()
{
SingleCommand result;
@ -527,6 +594,8 @@ BAN::ErrorOr<SingleCommand> TokenParser::parse_single_command()
case Token::Type::CloseCurly:
case Token::Type::Dollar:
case Token::Type::DoubleQuote:
case Token::Type::GreaterThan:
case Token::Type::LessThan:
case Token::Type::OpenCurly:
case Token::Type::OpenParen:
case Token::Type::SingleQuote:
@ -562,18 +631,56 @@ BAN::ErrorOr<SingleCommand> TokenParser::parse_single_command()
consume_token();
}
const auto can_parse_redirection =
[this]() -> bool
{
const auto& token = peek_token();
if (token.type() == Token::Type::GreaterThan)
return true;
if (token.type() == Token::Type::LessThan)
return true;
if (token.type() != Token::Type::String)
return false;
if (token.string().empty())
return false;
bool is_number = true;
for (size_t i = 0; i < token.string().size() && is_number; i++)
is_number = isdigit(token.string()[i]);
if (!is_number)
return false;
auto temp = read_token();
const bool is_redir =
(peek_token().type() == Token::Type::GreaterThan) ||
(peek_token().type() == Token::Type::LessThan);
MUST(unget_token(BAN::move(temp)));
if (!is_redir)
return false;
return true;
};
while (peek_token().type() != Token::Type::EOF_)
{
while (peek_token().type() == Token::Type::Whitespace)
consume_token();
auto argument = TRY(parse_argument());
TRY(result.arguments.push_back(BAN::move(argument)));
while (peek_token().type() == Token::Type::Whitespace)
consume_token();
if (!can_parse_argument_from_token_type(peek_token().type()))
if (can_parse_redirection())
{
auto redirection = TRY(parse_redirection());
TRY(result.redirections.push_back(BAN::move(redirection)));
}
else if (can_parse_argument_from_token_type(peek_token().type()))
{
auto argument = TRY(parse_argument());
TRY(result.arguments.push_back(BAN::move(argument)));
}
else
{
break;
}
}
return result;

View File

@ -44,6 +44,8 @@ private:
BAN::ErrorOr<CommandArgument::ArgumentPart> parse_single_quote();
BAN::ErrorOr<CommandArgument> parse_argument();
BAN::ErrorOr<SingleCommand::Redirection> parse_redirection();
BAN::ErrorOr<SingleCommand> parse_single_command();
BAN::ErrorOr<PipedCommand> parse_piped_command();
BAN::ErrorOr<CommandTree> parse_command_tree();