Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "Parser.h"
28#include <stdio.h>
29#include <unistd.h>
30#include <ctype.h>
31
32void Parser::commit_token(AllowEmptyToken allow_empty)
33{
34 if (allow_empty == AllowEmptyToken::No && m_token.is_empty())
35 return;
36 if (m_state == InRedirectionPath) {
37 m_redirections.last().path = String::copy(m_token);
38 m_token.clear_with_capacity();
39 return;
40 }
41 m_tokens.append(String::copy(m_token));
42 m_token.clear_with_capacity();
43};
44
45void Parser::commit_subcommand()
46{
47 if (m_tokens.is_empty())
48 return;
49 m_subcommands.append({ move(m_tokens), move(m_redirections), {} });
50}
51
52void Parser::commit_command()
53{
54 if (m_subcommands.is_empty())
55 return;
56 m_commands.append({ move(m_subcommands) });
57}
58
59void Parser::do_pipe()
60{
61 m_redirections.append({ Redirection::Pipe, STDOUT_FILENO });
62 commit_subcommand();
63}
64
65void Parser::begin_redirect_read(int fd)
66{
67 m_redirections.append({ Redirection::FileRead, fd });
68}
69
70void Parser::begin_redirect_write(int fd)
71{
72 m_redirections.append({ Redirection::FileWrite, fd });
73}
74
75Vector<Command> Parser::parse()
76{
77 for (size_t i = 0; i < m_input.length(); ++i) {
78 char ch = m_input.characters()[i];
79 switch (m_state) {
80 case State::Free:
81 if (ch == ' ') {
82 commit_token();
83 break;
84 }
85 if (ch == ';') {
86 commit_token();
87 commit_subcommand();
88 commit_command();
89 break;
90 }
91 if (ch == '|') {
92 commit_token();
93 if (m_tokens.is_empty()) {
94 fprintf(stderr, "Syntax error: Nothing before pipe (|)\n");
95 return {};
96 }
97 do_pipe();
98 break;
99 }
100 if (ch == '>') {
101 commit_token();
102 begin_redirect_write(STDOUT_FILENO);
103
104 // Search for another > for append.
105 m_state = State::InWriteAppendOrRedirectionPath;
106 break;
107 }
108 if (ch == '<') {
109 commit_token();
110 begin_redirect_read(STDIN_FILENO);
111 m_state = State::InRedirectionPath;
112 break;
113 }
114 if (ch == '\\') {
115 if (i == m_input.length() - 1) {
116 fprintf(stderr, "Syntax error: Nothing to escape (\\)\n");
117 return {};
118 }
119 char next_ch = m_input.characters()[i + 1];
120 m_token.append(next_ch);
121 ++i;
122 break;
123 }
124 if (ch == '\'') {
125 m_state = State::InSingleQuotes;
126 break;
127 }
128 if (ch == '\"') {
129 m_state = State::InDoubleQuotes;
130 break;
131 }
132
133 // redirection from zsh-style multi-digit fd, such as {10}>file
134 if (ch == '{') {
135 bool is_multi_fd_redirection = false;
136 size_t redir_end = i + 1;
137
138 while (redir_end < m_input.length()) {
139 char lookahead_ch = m_input.characters()[redir_end];
140 if (isdigit(lookahead_ch)) {
141 ++redir_end;
142 continue;
143 }
144 if (lookahead_ch == '}' && redir_end + 1 != m_input.length()) {
145 // Disallow {}> and {}<
146 if (redir_end == i + 1)
147 break;
148
149 ++redir_end;
150 if (m_input.characters()[redir_end] == '>' || m_input.characters()[redir_end] == '<')
151 is_multi_fd_redirection = true;
152 break;
153 }
154 break;
155 }
156
157 if (is_multi_fd_redirection) {
158 commit_token();
159
160 int fd = atoi(&m_input.characters()[i + 1]);
161
162 if (m_input.characters()[redir_end] == '>') {
163 begin_redirect_write(fd);
164 // Search for another > for append.
165 m_state = State::InWriteAppendOrRedirectionPath;
166 }
167 if (m_input.characters()[redir_end] == '<') {
168 begin_redirect_read(fd);
169 m_state = State::InRedirectionPath;
170 }
171
172 i = redir_end;
173
174 break;
175 }
176 }
177 if (isdigit(ch)) {
178 if (i != m_input.length() - 1) {
179 char next_ch = m_input.characters()[i + 1];
180 if (next_ch == '>') {
181 commit_token();
182 begin_redirect_write(ch - '0');
183 ++i;
184
185 // Search for another > for append.
186 m_state = State::InWriteAppendOrRedirectionPath;
187 break;
188 }
189 if (next_ch == '<') {
190 commit_token();
191 begin_redirect_read(ch - '0');
192 ++i;
193
194 m_state = State::InRedirectionPath;
195 break;
196 }
197 }
198 }
199 m_token.append(ch);
200 break;
201 case State::InWriteAppendOrRedirectionPath:
202 if (ch == '>') {
203 commit_token();
204 m_state = State::InRedirectionPath;
205 ASSERT(m_redirections.size());
206 m_redirections[m_redirections.size() - 1].type = Redirection::FileWriteAppend;
207 break;
208 }
209
210 // Not another > means that it's probably a path.
211 m_state = InRedirectionPath;
212 [[fallthrough]];
213 case State::InRedirectionPath:
214 if (ch == '<') {
215 commit_token();
216 begin_redirect_read(STDIN_FILENO);
217 m_state = State::InRedirectionPath;
218 break;
219 }
220 if (ch == '>') {
221 commit_token();
222 begin_redirect_read(STDOUT_FILENO);
223 m_state = State::InRedirectionPath;
224 break;
225 }
226 if (ch == '|') {
227 commit_token();
228 if (m_tokens.is_empty()) {
229 fprintf(stderr, "Syntax error: Nothing before pipe (|)\n");
230 return {};
231 }
232 do_pipe();
233 m_state = State::Free;
234 break;
235 }
236 if (ch == ' ')
237 break;
238 m_token.append(ch);
239 break;
240 case State::InSingleQuotes:
241 if (ch == '\'') {
242 commit_token(AllowEmptyToken::Yes);
243 m_state = State::Free;
244 break;
245 }
246 m_token.append(ch);
247 break;
248 case State::InDoubleQuotes:
249 if (ch == '\"') {
250 commit_token(AllowEmptyToken::Yes);
251 m_state = State::Free;
252 break;
253 }
254 if (ch == '\\') {
255 if (i == m_input.length() - 1) {
256 fprintf(stderr, "Syntax error: Nothing to escape (\\)\n");
257 return {};
258 }
259 char next_ch = m_input.characters()[i + 1];
260 if (next_ch == '$' || next_ch == '`'
261 || next_ch == '"' || next_ch == '\\') {
262 m_token.append(next_ch);
263 ++i;
264 continue;
265 }
266 m_token.append('\\');
267 break;
268 }
269 m_token.append(ch);
270 break;
271 };
272 }
273 commit_token();
274 commit_subcommand();
275 commit_command();
276
277 if (!m_subcommands.is_empty()) {
278 for (auto& redirection : m_subcommands.last().redirections) {
279 if (redirection.type == Redirection::Pipe) {
280 fprintf(stderr, "Syntax error: Nothing after last pipe (|)\n");
281 return {};
282 }
283 }
284 }
285
286 return move(m_commands);
287}