From c1978f9133d9ad314798c0fafd0b1bec743b658c Mon Sep 17 00:00:00 2001 From: Bananymous Date: Tue, 22 Jul 2025 16:54:06 +0300 Subject: [PATCH] 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. --- userspace/programs/Shell/CommandTypes.h | 19 +++- userspace/programs/Shell/Execute.cpp | 60 ++++++++++++ userspace/programs/Shell/Execute.h | 11 +++ userspace/programs/Shell/Lexer.cpp | 2 + userspace/programs/Shell/Token.cpp | 6 ++ userspace/programs/Shell/Token.h | 2 + userspace/programs/Shell/TokenParser.cpp | 119 +++++++++++++++++++++-- userspace/programs/Shell/TokenParser.h | 2 + 8 files changed, 212 insertions(+), 9 deletions(-) diff --git a/userspace/programs/Shell/CommandTypes.h b/userspace/programs/Shell/CommandTypes.h index 17105064..3cdad533 100644 --- a/userspace/programs/Shell/CommandTypes.h +++ b/userspace/programs/Shell/CommandTypes.h @@ -2,7 +2,7 @@ #include -#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 environment; BAN::Vector arguments; + BAN::Vector redirections; }; struct PipedCommand diff --git a/userspace/programs/Shell/Execute.cpp b/userspace/programs/Shell/Execute.cpp index eb82d207..0e23a867 100644 --- a/userspace/programs/Shell/Execute.cpp +++ b/userspace/programs/Shell/Execute.cpp @@ -99,6 +99,47 @@ BAN::ErrorOr 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().data(), const_cast(exec_args.data())); perror("execv"); exit(errno); @@ -125,6 +166,7 @@ BAN::ErrorOr Execute::execute_command_sync(BAN::Span arg .command = {}, .arguments = arguments, .environments = {}, + .redirections = {}, .fd_in = fd_in, .fd_out = fd_out, .background = false, @@ -194,6 +236,22 @@ BAN::ErrorOr Execute::execute_command(const PipedCommand& piped_command) return result; }; + const auto evaluate_redirections = + [this](BAN::Span redirections) -> BAN::ErrorOr> + { + BAN::Vector 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 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, diff --git a/userspace/programs/Shell/Execute.h b/userspace/programs/Shell/Execute.h index 6bec3fc1..0cb1766b 100644 --- a/userspace/programs/Shell/Execute.h +++ b/userspace/programs/Shell/Execute.h @@ -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 execute_command_sync(BAN::Span arguments, int fd_in, int fd_out); BAN::ErrorOr execute_command(const SingleCommand&, int fd_in, int fd_out, bool background, pid_t pgrp = 0); BAN::ErrorOr 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 arguments; BAN::Span environments; + BAN::Span redirections; int fd_in; int fd_out; bool background; diff --git a/userspace/programs/Shell/Lexer.cpp b/userspace/programs/Shell/Lexer.cpp index a87c5715..d1c538ae 100644 --- a/userspace/programs/Shell/Lexer.cpp +++ b/userspace/programs/Shell/Lexer.cpp @@ -21,6 +21,8 @@ BAN::ErrorOr> 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; diff --git a/userspace/programs/Shell/Token.cpp b/userspace/programs/Shell/Token.cpp index 77c07185..549ecd04 100644 --- a/userspace/programs/Shell/Token.cpp +++ b/userspace/programs/Shell/Token.cpp @@ -27,6 +27,12 @@ void Token::debug_dump() const case Type::DoubleQuote: dprintln("Token "); break; + case Type::GreaterThan: + dprintln("Token "); + break; + case Type::LessThan: + dprintln("Token "); + break; case Type::OpenCurly: dprintln("Token "); break; diff --git a/userspace/programs/Shell/Token.h b/userspace/programs/Shell/Token.h index aa59d3d5..f86b4888 100644 --- a/userspace/programs/Shell/Token.h +++ b/userspace/programs/Shell/Token.h @@ -19,6 +19,8 @@ public: CloseParen, Dollar, DoubleQuote, + GreaterThan, + LessThan, OpenCurly, OpenParen, Pipe, diff --git a/userspace/programs/Shell/TokenParser.cpp b/userspace/programs/Shell/TokenParser.cpp index 28d0c41e..97f0db12 100644 --- a/userspace/programs/Shell/TokenParser.cpp +++ b/userspace/programs/Shell/TokenParser.cpp @@ -7,6 +7,7 @@ #include #include +#include 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 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 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 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 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 TokenParser::parse_argument() return result; } +BAN::ErrorOr 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 TokenParser::parse_single_command() { SingleCommand result; @@ -527,6 +594,8 @@ BAN::ErrorOr 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 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; diff --git a/userspace/programs/Shell/TokenParser.h b/userspace/programs/Shell/TokenParser.h index e48ee2a0..d65252a1 100644 --- a/userspace/programs/Shell/TokenParser.h +++ b/userspace/programs/Shell/TokenParser.h @@ -44,6 +44,8 @@ private: BAN::ErrorOr parse_single_quote(); BAN::ErrorOr parse_argument(); + BAN::ErrorOr parse_redirection(); + BAN::ErrorOr parse_single_command(); BAN::ErrorOr parse_piped_command(); BAN::ErrorOr parse_command_tree();