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 "GlobalState.h"
28#include "Parser.h"
29#include <AK/FileSystemPath.h>
30#include <AK/Function.h>
31#include <AK/StringBuilder.h>
32#include <LibCore/DirIterator.h>
33#include <LibCore/ElapsedTimer.h>
34#include <LibCore/File.h>
35#include <LibLine/Editor.h>
36#include <errno.h>
37#include <fcntl.h>
38#include <limits.h>
39#include <pwd.h>
40#include <signal.h>
41#include <stdio.h>
42#include <stdlib.h>
43#include <string.h>
44#include <sys/mman.h>
45#include <sys/stat.h>
46#include <sys/utsname.h>
47#include <sys/wait.h>
48#include <termios.h>
49#include <unistd.h>
50
51//#define SH_DEBUG
52
53GlobalState g;
54static Line::Editor editor {};
55
56static int run_command(const String&);
57void cache_path();
58
59#ifndef __serenity__
60extern char **environ;
61#endif
62
63static String prompt()
64{
65 auto* ps1 = getenv("PROMPT");
66 if (!ps1) {
67 if (g.uid == 0)
68 return "# ";
69
70 StringBuilder builder;
71 builder.appendf("\033]0;%s@%s:%s\007", g.username.characters(), g.hostname, g.cwd.characters());
72 builder.appendf("\033[31;1m%s\033[0m@\033[37;1m%s\033[0m:\033[32;1m%s\033[0m$> ", g.username.characters(), g.hostname, g.cwd.characters());
73 return builder.to_string();
74 }
75
76 StringBuilder builder;
77 for (char* ptr = ps1; *ptr; ++ptr) {
78 if (*ptr == '\\') {
79 ++ptr;
80 if (!*ptr)
81 break;
82 switch (*ptr) {
83 case 'X':
84 builder.append("\033]0;");
85 break;
86 case 'a':
87 builder.append(0x07);
88 break;
89 case 'e':
90 builder.append(0x1b);
91 break;
92 case 'u':
93 builder.append(g.username);
94 break;
95 case 'h':
96 builder.append(g.hostname);
97 break;
98 case 'w': {
99 String home_path = getenv("HOME");
100 if (g.cwd.starts_with(home_path)) {
101 builder.append('~');
102 builder.append(g.cwd.substring_view(home_path.length(), g.cwd.length() - home_path.length()));
103 } else {
104 builder.append(g.cwd);
105 }
106 break;
107 }
108 case 'p':
109 builder.append(g.uid == 0 ? '#' : '$');
110 break;
111 }
112 continue;
113 }
114 builder.append(*ptr);
115 }
116 return builder.to_string();
117}
118
119static int sh_pwd(int, const char**)
120{
121 printf("%s\n", g.cwd.characters());
122 return 0;
123}
124
125static int sh_exit(int, const char**)
126{
127 printf("Good-bye!\n");
128 exit(0);
129 return 0;
130}
131
132static int sh_export(int argc, const char** argv)
133{
134 if (argc == 1) {
135 for (int i = 0; environ[i]; ++i)
136 puts(environ[i]);
137 return 0;
138 }
139 auto parts = String(argv[1]).split('=');
140 if (parts.size() != 2) {
141 fprintf(stderr, "usage: export variable=value\n");
142 return 1;
143 }
144
145 int setenv_return = setenv(parts[0].characters(), parts[1].characters(), 1);
146
147 if (setenv_return == 0 && parts[0] == "PATH")
148 cache_path();
149
150 return setenv_return;
151}
152
153static int sh_unset(int argc, const char** argv)
154{
155 if (argc != 2) {
156 fprintf(stderr, "usage: unset variable\n");
157 return 1;
158 }
159
160 unsetenv(argv[1]);
161 return 0;
162}
163
164static String expand_tilde(const String& expression)
165{
166 ASSERT(expression.starts_with('~'));
167
168 StringBuilder login_name;
169 size_t first_slash_index = expression.length();
170 for (size_t i = 1; i < expression.length(); ++i) {
171 if (expression[i] == '/') {
172 first_slash_index = i;
173 break;
174 }
175 login_name.append(expression[i]);
176 }
177
178 StringBuilder path;
179 for (size_t i = first_slash_index; i < expression.length(); ++i)
180 path.append(expression[i]);
181
182 if (login_name.is_empty()) {
183 const char* home = getenv("HOME");
184 if (!home) {
185 auto passwd = getpwuid(getuid());
186 ASSERT(passwd && passwd->pw_dir);
187 return String::format("%s/%s", passwd->pw_dir, path.to_string().characters());
188 }
189 return String::format("%s/%s", home, path.to_string().characters());
190 }
191
192 auto passwd = getpwnam(login_name.to_string().characters());
193 if (!passwd)
194 return expression;
195 ASSERT(passwd->pw_dir);
196
197 return String::format("%s/%s", passwd->pw_dir, path.to_string().characters());
198}
199
200static int sh_cd(int argc, const char** argv)
201{
202 if (argc > 2) {
203 fprintf(stderr, "cd: too many arguments\n");
204 return 1;
205 }
206
207 String new_path;
208
209 if (argc == 1) {
210 new_path = g.home;
211 if (g.cd_history.is_empty() || g.cd_history.last() != g.home)
212 g.cd_history.enqueue(g.home);
213 } else {
214 if (g.cd_history.is_empty() || g.cd_history.last() != argv[1])
215 g.cd_history.enqueue(argv[1]);
216 if (strcmp(argv[1], "-") == 0) {
217 char* oldpwd = getenv("OLDPWD");
218 if (oldpwd == nullptr)
219 return 1;
220 new_path = oldpwd;
221 } else if (argv[1][0] == '/') {
222 new_path = argv[1];
223 } else {
224 StringBuilder builder;
225 builder.append(g.cwd);
226 builder.append('/');
227 builder.append(argv[1]);
228 new_path = builder.to_string();
229 }
230 }
231
232 FileSystemPath canonical_path(new_path);
233 if (!canonical_path.is_valid()) {
234 printf("FileSystemPath failed to canonicalize '%s'\n", new_path.characters());
235 return 1;
236 }
237 const char* path = canonical_path.string().characters();
238
239 struct stat st;
240 int rc = stat(path, &st);
241 if (rc < 0) {
242 printf("stat(%s) failed: %s\n", path, strerror(errno));
243 return 1;
244 }
245 if (!S_ISDIR(st.st_mode)) {
246 printf("Not a directory: %s\n", path);
247 return 1;
248 }
249 rc = chdir(path);
250 if (rc < 0) {
251 printf("chdir(%s) failed: %s\n", path, strerror(errno));
252 return 1;
253 }
254 setenv("OLDPWD", g.cwd.characters(), 1);
255 g.cwd = canonical_path.string();
256 setenv("PWD", g.cwd.characters(), 1);
257 return 0;
258}
259
260static int sh_cdh(int argc, const char** argv)
261{
262 if (argc > 2) {
263 fprintf(stderr, "usage: cdh [index]\n");
264 return 1;
265 }
266
267 if (argc == 1) {
268 if (g.cd_history.size() == 0) {
269 printf("cdh: no history available\n");
270 return 0;
271 }
272
273 for (int i = g.cd_history.size() - 1; i >= 0; --i)
274 printf("%lu: %s\n", g.cd_history.size() - i, g.cd_history.at(i).characters());
275 return 0;
276 }
277
278 bool ok;
279 size_t cd_history_index = String(argv[1]).to_uint(ok);
280
281 if (!ok || cd_history_index < 1 || cd_history_index > g.cd_history.size()) {
282 fprintf(stderr, "usage: cdh [index]\n");
283 return 1;
284 }
285
286 const char* path = g.cd_history.at(g.cd_history.size() - cd_history_index).characters();
287 const char* cd_args[] = { "cd", path };
288 return sh_cd(2, cd_args);
289}
290
291static int sh_history(int, const char**)
292{
293 for (size_t i = 0; i < editor.history().size(); ++i) {
294 printf("%6zu %s\n", i, editor.history()[i].characters());
295 }
296 return 0;
297}
298
299static int sh_time(int argc, const char** argv)
300{
301 if (argc == 1) {
302 printf("usage: time <command>\n");
303 return 0;
304 }
305 StringBuilder builder;
306 for (int i = 1; i < argc; ++i) {
307 builder.append(argv[i]);
308 if (i != argc - 1)
309 builder.append(' ');
310 }
311 Core::ElapsedTimer timer;
312 timer.start();
313 int exit_code = run_command(builder.to_string());
314 printf("Time: %d ms\n", timer.elapsed());
315 return exit_code;
316}
317
318static int sh_umask(int argc, const char** argv)
319{
320 if (argc == 1) {
321 mode_t old_mask = umask(0);
322 printf("%#o\n", old_mask);
323 umask(old_mask);
324 return 0;
325 }
326 if (argc == 2) {
327 unsigned mask;
328 int matches = sscanf(argv[1], "%o", &mask);
329 if (matches == 1) {
330 umask(mask);
331 return 0;
332 }
333 }
334 printf("usage: umask <octal-mask>\n");
335 return 0;
336}
337
338static int sh_popd(int argc, const char** argv)
339{
340 if (g.directory_stack.size() <= 1) {
341 fprintf(stderr, "Shell: popd: directory stack empty\n");
342 return 1;
343 }
344
345 bool should_switch = true;
346 String path = g.directory_stack.take_last();
347
348 // When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory.
349 if (argc == 1) {
350 int rc = chdir(path.characters());
351 if (rc < 0) {
352 fprintf(stderr, "chdir(%s) failed: %s", path.characters(), strerror(errno));
353 return 1;
354 }
355
356 g.cwd = path;
357 return 0;
358 }
359
360 for (int i = 1; i < argc; i++) {
361 const char* arg = argv[i];
362 if (!strcmp(arg, "-n")) {
363 should_switch = false;
364 }
365 }
366
367 FileSystemPath canonical_path(path.characters());
368 if (!canonical_path.is_valid()) {
369 fprintf(stderr, "FileSystemPath failed to canonicalize '%s'\n", path.characters());
370 return 1;
371 }
372
373 const char* real_path = canonical_path.string().characters();
374
375 struct stat st;
376 int rc = stat(real_path, &st);
377 if (rc < 0) {
378 fprintf(stderr, "stat(%s) failed: %s\n", real_path, strerror(errno));
379 return 1;
380 }
381
382 if (!S_ISDIR(st.st_mode)) {
383 fprintf(stderr, "Not a directory: %s\n", real_path);
384 return 1;
385 }
386
387 if (should_switch) {
388 int rc = chdir(real_path);
389 if (rc < 0) {
390 fprintf(stderr, "chdir(%s) failed: %s\n", real_path, strerror(errno));
391 return 1;
392 }
393
394 g.cwd = canonical_path.string();
395 }
396
397 return 0;
398}
399
400static int sh_pushd(int argc, const char** argv)
401{
402 StringBuilder path_builder;
403 bool should_switch = true;
404
405 // From the BASH reference manual: https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html
406 // With no arguments, pushd exchanges the top two directories and makes the new top the current directory.
407 if (argc == 1) {
408 if (g.directory_stack.size() < 2) {
409 fprintf(stderr, "pushd: no other directory\n");
410 return 1;
411 }
412
413 String dir1 = g.directory_stack.take_first();
414 String dir2 = g.directory_stack.take_first();
415 g.directory_stack.insert(0, dir2);
416 g.directory_stack.insert(1, dir1);
417
418 int rc = chdir(dir2.characters());
419 if (rc < 0) {
420 fprintf(stderr, "chdir(%s) failed: %s", dir2.characters(), strerror(errno));
421 return 1;
422 }
423
424 g.cwd = dir2;
425
426 return 0;
427 }
428
429 // Let's assume the user's typed in 'pushd <dir>'
430 if (argc == 2) {
431 g.directory_stack.append(g.cwd.characters());
432 if (argv[1][0] == '/') {
433 path_builder.append(argv[1]);
434 } else {
435 path_builder.appendf("%s/%s", g.cwd.characters(), argv[1]);
436 }
437 } else if (argc == 3) {
438 g.directory_stack.append(g.cwd.characters());
439 for (int i = 1; i < argc; i++) {
440 const char* arg = argv[i];
441
442 if (arg[0] != '-') {
443 if (arg[0] == '/') {
444 path_builder.append(arg);
445 } else
446 path_builder.appendf("%s/%s", g.cwd.characters(), arg);
447 }
448
449 if (!strcmp(arg, "-n"))
450 should_switch = false;
451 }
452 }
453
454 FileSystemPath canonical_path(path_builder.to_string());
455 if (!canonical_path.is_valid()) {
456 fprintf(stderr, "FileSystemPath failed to canonicalize '%s'\n", path_builder.to_string().characters());
457 return 1;
458 }
459
460 const char* real_path = canonical_path.string().characters();
461
462 struct stat st;
463 int rc = stat(real_path, &st);
464 if (rc < 0) {
465 fprintf(stderr, "stat(%s) failed: %s\n", real_path, strerror(errno));
466 return 1;
467 }
468
469 if (!S_ISDIR(st.st_mode)) {
470 fprintf(stderr, "Not a directory: %s\n", real_path);
471 return 1;
472 }
473
474 if (should_switch) {
475 int rc = chdir(real_path);
476 if (rc < 0) {
477 fprintf(stderr, "chdir(%s) failed: %s\n", real_path, strerror(errno));
478 return 1;
479 }
480
481 g.cwd = canonical_path.string();
482 }
483
484 return 0;
485}
486
487static int sh_dirs(int argc, const char** argv)
488{
489 // The first directory in the stack is ALWAYS the current directory
490 g.directory_stack.at(0) = g.cwd.characters();
491
492 if (argc == 1) {
493 for (String dir : g.directory_stack)
494 printf("%s ", dir.characters());
495
496 printf("\n");
497 return 0;
498 }
499
500 bool printed = false;
501 for (int i = 0; i < argc; i++) {
502 const char* arg = argv[i];
503 if (!strcmp(arg, "-c")) {
504 for (size_t i = 1; i < g.directory_stack.size(); i++)
505 g.directory_stack.remove(i);
506
507 printed = true;
508 continue;
509 }
510 if (!strcmp(arg, "-p") && !printed) {
511 for (auto& directory : g.directory_stack)
512 printf("%s\n", directory.characters());
513
514 printed = true;
515 continue;
516 }
517 if (!strcmp(arg, "-v") && !printed) {
518 int idx = 0;
519 for (auto& directory : g.directory_stack) {
520 printf("%d %s\n", idx++, directory.characters());
521 }
522
523 printed = true;
524 continue;
525 }
526 }
527
528 return 0;
529}
530
531static bool handle_builtin(int argc, const char** argv, int& retval)
532{
533 if (argc == 0)
534 return false;
535 if (!strcmp(argv[0], "cd")) {
536 retval = sh_cd(argc, argv);
537 return true;
538 }
539 if (!strcmp(argv[0], "cdh")) {
540 retval = sh_cdh(argc, argv);
541 return true;
542 }
543 if (!strcmp(argv[0], "pwd")) {
544 retval = sh_pwd(argc, argv);
545 return true;
546 }
547 if (!strcmp(argv[0], "exit")) {
548 retval = sh_exit(argc, argv);
549 return true;
550 }
551 if (!strcmp(argv[0], "export")) {
552 retval = sh_export(argc, argv);
553 return true;
554 }
555 if (!strcmp(argv[0], "unset")) {
556 retval = sh_unset(argc, argv);
557 return true;
558 }
559 if (!strcmp(argv[0], "history")) {
560 retval = sh_history(argc, argv);
561 return true;
562 }
563 if (!strcmp(argv[0], "umask")) {
564 retval = sh_umask(argc, argv);
565 return true;
566 }
567 if (!strcmp(argv[0], "dirs")) {
568 retval = sh_dirs(argc, argv);
569 return true;
570 }
571 if (!strcmp(argv[0], "pushd")) {
572 retval = sh_pushd(argc, argv);
573 return true;
574 }
575 if (!strcmp(argv[0], "popd")) {
576 retval = sh_popd(argc, argv);
577 return true;
578 }
579 if (!strcmp(argv[0], "time")) {
580 retval = sh_time(argc, argv);
581 return true;
582 }
583 return false;
584}
585
586class FileDescriptionCollector {
587public:
588 FileDescriptionCollector() {}
589 ~FileDescriptionCollector() { collect(); }
590
591 void collect()
592 {
593 for (auto fd : m_fds)
594 close(fd);
595 m_fds.clear();
596 }
597 void add(int fd) { m_fds.append(fd); }
598
599private:
600 Vector<int, 32> m_fds;
601};
602
603class CommandTimer {
604public:
605 explicit CommandTimer(const String& command)
606 : m_command(command)
607 {
608 m_timer.start();
609 }
610 ~CommandTimer()
611 {
612 dbg() << "Command \"" << m_command << "\" finished in " << m_timer.elapsed() << " ms";
613 }
614
615private:
616 Core::ElapsedTimer m_timer;
617 String m_command;
618};
619
620static bool is_glob(const StringView& s)
621{
622 for (size_t i = 0; i < s.length(); i++) {
623 char c = s.characters_without_null_termination()[i];
624 if (c == '*' || c == '?')
625 return true;
626 }
627 return false;
628}
629
630static Vector<StringView> split_path(const StringView& path)
631{
632 Vector<StringView> parts;
633
634 size_t substart = 0;
635 for (size_t i = 0; i < path.length(); i++) {
636 char ch = path.characters_without_null_termination()[i];
637 if (ch != '/')
638 continue;
639 size_t sublen = i - substart;
640 if (sublen != 0)
641 parts.append(path.substring_view(substart, sublen));
642 parts.append(path.substring_view(i, 1));
643 substart = i + 1;
644 }
645
646 size_t taillen = path.length() - substart;
647 if (taillen != 0)
648 parts.append(path.substring_view(substart, taillen));
649
650 return parts;
651}
652
653static Vector<String> expand_globs(const StringView& path, const StringView& base)
654{
655 auto parts = split_path(path);
656
657 StringBuilder builder;
658 builder.append(base);
659 Vector<String> res;
660
661 for (size_t i = 0; i < parts.size(); ++i) {
662 auto& part = parts[i];
663 if (!is_glob(part)) {
664 builder.append(part);
665 continue;
666 }
667
668 // Found a glob.
669 String new_base = builder.to_string();
670 StringView new_base_v = new_base;
671 if (new_base_v.is_empty())
672 new_base_v = ".";
673 Core::DirIterator di(new_base_v, Core::DirIterator::SkipParentAndBaseDir);
674
675 if (di.has_error()) {
676 return res;
677 }
678
679 while (di.has_next()) {
680 String name = di.next_path();
681
682 // Dotfiles have to be explicitly requested
683 if (name[0] == '.' && part[0] != '.')
684 continue;
685
686 if (name.matches(part, CaseSensitivity::CaseSensitive)) {
687
688 StringBuilder nested_base;
689 nested_base.append(new_base);
690 nested_base.append(name);
691
692 StringView remaining_path = path.substring_view_starting_after_substring(part);
693 Vector<String> nested_res = expand_globs(remaining_path, nested_base.to_string());
694 for (auto& s : nested_res)
695 res.append(s);
696 }
697 }
698 return res;
699 }
700
701 // Found no globs.
702 String new_path = builder.to_string();
703 if (access(new_path.characters(), F_OK) == 0)
704 res.append(new_path);
705 return res;
706}
707
708static Vector<String> expand_parameters(const StringView& param)
709{
710 if (!param.starts_with('$'))
711 return { param };
712
713 String variable_name = String(param.substring_view(1, param.length() - 1));
714 if (variable_name == "?")
715 return { String::number(g.last_return_code) };
716 else if (variable_name == "$")
717 return { String::number(getpid()) };
718
719 char* env_value = getenv(variable_name.characters());
720 if (env_value == nullptr)
721 return { "" };
722
723 Vector<String> res;
724 String str_env_value = String(env_value);
725 const auto& split_text = str_env_value.split_view(' ');
726 for (auto& part : split_text)
727 res.append(part);
728 return res;
729}
730
731static Vector<String> process_arguments(const Vector<String>& args)
732{
733 Vector<String> argv_string;
734 for (auto& arg : args) {
735 // This will return the text passed in if it wasn't a variable
736 // This lets us just loop over its values
737 auto expanded_parameters = expand_parameters(arg);
738
739 for (auto& exp_arg : expanded_parameters) {
740 if (exp_arg.starts_with('~'))
741 exp_arg = expand_tilde(exp_arg);
742
743 auto expanded_globs = expand_globs(exp_arg, "");
744 for (auto& path : expanded_globs)
745 argv_string.append(path);
746
747 if (expanded_globs.is_empty())
748 argv_string.append(exp_arg);
749 }
750 }
751
752 return argv_string;
753}
754
755static int run_command(const String& cmd)
756{
757 if (cmd.is_empty())
758 return 0;
759
760 if (cmd.starts_with("#"))
761 return 0;
762
763 auto commands = Parser(cmd).parse();
764
765#ifdef SH_DEBUG
766 for (auto& command : commands) {
767 for (size_t i = 0; i < command.subcommands.size(); ++i) {
768 for (size_t j = 0; j < i; ++j)
769 dbgprintf(" ");
770 for (auto& arg : command.subcommands[i].args) {
771 dbgprintf("<%s> ", arg.characters());
772 }
773 dbgprintf("\n");
774 for (auto& redirecton : command.subcommands[i].redirections) {
775 for (size_t j = 0; j < i; ++j)
776 dbgprintf(" ");
777 dbgprintf(" ");
778 switch (redirecton.type) {
779 case Redirection::Pipe:
780 dbgprintf("Pipe\n");
781 break;
782 case Redirection::FileRead:
783 dbgprintf("fd:%d = FileRead: %s\n", redirecton.fd, redirecton.path.characters());
784 break;
785 case Redirection::FileWrite:
786 dbgprintf("fd:%d = FileWrite: %s\n", redirecton.fd, redirecton.path.characters());
787 break;
788 case Redirection::FileWriteAppend:
789 dbgprintf("fd:%d = FileWriteAppend: %s\n", redirecton.fd, redirecton.path.characters());
790 break;
791 default:
792 break;
793 }
794 }
795 }
796 dbgprintf("\n");
797 }
798#endif
799
800 struct termios trm;
801 tcgetattr(0, &trm);
802
803 struct SpawnedProcess {
804 String name;
805 pid_t pid;
806 };
807
808 int return_value = 0;
809
810 for (auto& command : commands) {
811 if (command.subcommands.is_empty())
812 continue;
813
814 FileDescriptionCollector fds;
815
816 for (size_t i = 0; i < command.subcommands.size(); ++i) {
817 auto& subcommand = command.subcommands[i];
818 for (auto& redirection : subcommand.redirections) {
819 switch (redirection.type) {
820 case Redirection::Pipe: {
821 int pipefd[2];
822 int rc = pipe(pipefd);
823 if (rc < 0) {
824 perror("pipe");
825 return 1;
826 }
827 subcommand.rewirings.append({ STDOUT_FILENO, pipefd[1] });
828 auto& next_command = command.subcommands[i + 1];
829 next_command.rewirings.append({ STDIN_FILENO, pipefd[0] });
830 fds.add(pipefd[0]);
831 fds.add(pipefd[1]);
832 break;
833 }
834 case Redirection::FileWriteAppend: {
835 int fd = open(redirection.path.characters(), O_WRONLY | O_CREAT | O_APPEND, 0666);
836 if (fd < 0) {
837 perror("open");
838 return 1;
839 }
840 subcommand.rewirings.append({ redirection.fd, fd });
841 fds.add(fd);
842 break;
843 }
844 case Redirection::FileWrite: {
845 int fd = open(redirection.path.characters(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
846 if (fd < 0) {
847 perror("open");
848 return 1;
849 }
850 subcommand.rewirings.append({ redirection.fd, fd });
851 fds.add(fd);
852 break;
853 }
854 case Redirection::FileRead: {
855 int fd = open(redirection.path.characters(), O_RDONLY);
856 if (fd < 0) {
857 perror("open");
858 return 1;
859 }
860 subcommand.rewirings.append({ redirection.fd, fd });
861 fds.add(fd);
862 break;
863 }
864 }
865 }
866 }
867
868 Vector<SpawnedProcess> children;
869
870 CommandTimer timer(cmd);
871
872 for (size_t i = 0; i < command.subcommands.size(); ++i) {
873 auto& subcommand = command.subcommands[i];
874 Vector<String> argv_string = process_arguments(subcommand.args);
875 Vector<const char*> argv;
876 argv.ensure_capacity(argv_string.size());
877 for (const auto& s : argv_string) {
878 argv.append(s.characters());
879 }
880 argv.append(nullptr);
881
882#ifdef SH_DEBUG
883 for (auto& arg : argv) {
884 dbgprintf("<%s> ", arg);
885 }
886 dbgprintf("\n");
887#endif
888
889 int retval = 0;
890 if (handle_builtin(argv.size() - 1, argv.data(), retval))
891 return retval;
892
893 pid_t child = fork();
894 if (!child) {
895 setpgid(0, 0);
896 tcsetpgrp(0, getpid());
897 tcsetattr(0, TCSANOW, &g.default_termios);
898 for (auto& rewiring : subcommand.rewirings) {
899#ifdef SH_DEBUG
900 dbgprintf("in %s<%d>, dup2(%d, %d)\n", argv[0], getpid(), rewiring.rewire_fd, rewiring.fd);
901#endif
902 int rc = dup2(rewiring.rewire_fd, rewiring.fd);
903 if (rc < 0) {
904 perror("dup2");
905 return 1;
906 }
907 }
908
909 fds.collect();
910
911 int rc = execvp(argv[0], const_cast<char* const*>(argv.data()));
912 if (rc < 0) {
913 if (errno == ENOENT)
914 fprintf(stderr, "%s: Command not found.\n", argv[0]);
915 else
916 fprintf(stderr, "execvp(%s): %s\n", argv[0], strerror(errno));
917 _exit(1);
918 }
919 ASSERT_NOT_REACHED();
920 }
921 children.append({ argv[0], child });
922 }
923
924#ifdef SH_DEBUG
925 dbgprintf("Closing fds in shell process:\n");
926#endif
927 fds.collect();
928
929#ifdef SH_DEBUG
930 dbgprintf("Now we gotta wait on children:\n");
931 for (auto& child : children)
932 dbgprintf(" %d (%s)\n", child.pid, child.name.characters());
933#endif
934
935 int wstatus = 0;
936
937 for (size_t i = 0; i < children.size(); ++i) {
938 auto& child = children[i];
939 do {
940 int rc = waitpid(child.pid, &wstatus, 0);
941 if (rc < 0 && errno != EINTR) {
942 if (errno != ECHILD)
943 perror("waitpid");
944 break;
945 }
946 if (WIFEXITED(wstatus)) {
947 if (WEXITSTATUS(wstatus) != 0)
948 dbg() << "Shell: " << child.name << ":" << child.pid << " exited with status " << WEXITSTATUS(wstatus);
949 if (i == 0)
950 return_value = WEXITSTATUS(wstatus);
951 } else if (WIFSTOPPED(wstatus)) {
952 fprintf(stderr, "Shell: %s(%d) %s\n", child.name.characters(), child.pid, strsignal(WSTOPSIG(wstatus)));
953 } else {
954 if (WIFSIGNALED(wstatus)) {
955 printf("Shell: %s(%d) exited due to signal '%s'\n", child.name.characters(), child.pid, strsignal(WTERMSIG(wstatus)));
956 } else {
957 printf("Shell: %s(%d) exited abnormally\n", child.name.characters(), child.pid);
958 }
959 }
960 } while (errno == EINTR);
961 }
962 }
963
964 g.last_return_code = return_value;
965
966 // FIXME: Should I really have to tcsetpgrp() after my child has exited?
967 // Is the terminal controlling pgrp really still the PGID of the dead process?
968 tcsetpgrp(0, getpid());
969 tcsetattr(0, TCSANOW, &trm);
970 return return_value;
971}
972
973static String get_history_path()
974{
975 StringBuilder builder;
976 builder.append(g.home);
977 builder.append("/.history");
978 return builder.to_string();
979}
980
981void load_history()
982{
983 auto history_file = Core::File::construct(get_history_path());
984 if (!history_file->open(Core::IODevice::ReadOnly))
985 return;
986 while (history_file->can_read_line()) {
987 auto b = history_file->read_line(1024);
988 // skip the newline and terminating bytes
989 editor.add_to_history(String(reinterpret_cast<const char*>(b.data()), b.size() - 2));
990 }
991}
992
993void save_history()
994{
995 auto history_file = Core::File::open(get_history_path(), Core::IODevice::WriteOnly, 0600);
996 if (!history_file)
997 return;
998 for (const auto& line : editor.history()) {
999 history_file->write(line);
1000 history_file->write("\n");
1001 }
1002}
1003
1004Vector<String, 256> cached_path;
1005void cache_path()
1006{
1007 if (!cached_path.is_empty())
1008 cached_path.clear_with_capacity();
1009
1010 String path = getenv("PATH");
1011 if (path.is_empty())
1012 return;
1013
1014 auto directories = path.split(':');
1015 for (const auto& directory : directories) {
1016 Core::DirIterator programs(directory.characters(), Core::DirIterator::SkipDots);
1017 while (programs.has_next()) {
1018 auto program = programs.next_path();
1019 String program_path = String::format("%s/%s", directory.characters(), program.characters());
1020 struct stat program_status;
1021 int stat_error = stat(program_path.characters(), &program_status);
1022 if (!stat_error && (program_status.st_mode & S_IXUSR))
1023 cached_path.append(program.characters());
1024 }
1025 }
1026
1027 quick_sort(cached_path);
1028}
1029
1030int main(int argc, char** argv)
1031{
1032#ifdef __serenity__
1033 if (pledge("stdio rpath wpath cpath proc exec tty", nullptr) < 0) {
1034 perror("pledge");
1035 return 1;
1036 }
1037#endif
1038
1039 g.uid = getuid();
1040 tcsetpgrp(0, getpgrp());
1041
1042 editor.initialize();
1043 g.termios = editor.termios();
1044 g.default_termios = editor.default_termios();
1045
1046 editor.on_tab_complete_first_token = [&](const String& token) -> Vector<String> {
1047 auto match = binary_search(cached_path.data(), cached_path.size(), token, [](const String& token, const String& program) -> int {
1048 return strncmp(token.characters(), program.characters(), token.length());
1049 });
1050 if (!match)
1051 return Vector<String>();
1052
1053 String completion = *match;
1054 Vector<String> suggestions;
1055
1056 // Now that we have a program name starting with our token, we look at
1057 // other program names starting with our token and cut off any mismatching
1058 // characters.
1059
1060 bool seen_others = false;
1061 int index = match - cached_path.data();
1062 for (int i = index - 1; i >= 0 && cached_path[i].starts_with(token); --i) {
1063 suggestions.append(cached_path[i]);
1064 editor.cut_mismatching_chars(completion, cached_path[i], token.length());
1065 seen_others = true;
1066 }
1067 for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(token); ++i) {
1068 editor.cut_mismatching_chars(completion, cached_path[i], token.length());
1069 suggestions.append(cached_path[i]);
1070 seen_others = true;
1071 }
1072 suggestions.append(cached_path[index]);
1073
1074 // If we have characters to add, add them.
1075 if (completion.length() > token.length())
1076 editor.insert(completion.substring(token.length(), completion.length() - token.length()));
1077 // If we have a single match, we add a space, unless we already have one.
1078 if (!seen_others && (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' '))
1079 editor.insert(' ');
1080
1081 return suggestions;
1082 };
1083 editor.on_tab_complete_other_token = [&](const String& vtoken) -> Vector<String> {
1084 auto token = vtoken; // copy it :(
1085 String path;
1086 Vector<String> suggestions;
1087
1088 ssize_t last_slash = token.length() - 1;
1089 while (last_slash >= 0 && token[last_slash] != '/')
1090 --last_slash;
1091
1092 if (last_slash >= 0) {
1093 // Split on the last slash. We'll use the first part as the directory
1094 // to search and the second part as the token to complete.
1095 path = token.substring(0, last_slash + 1);
1096 if (path[0] != '/')
1097 path = String::format("%s/%s", g.cwd.characters(), path.characters());
1098 path = canonicalized_path(path);
1099 token = token.substring(last_slash + 1, token.length() - last_slash - 1);
1100 } else {
1101 // We have no slashes, so the directory to search is the current
1102 // directory and the token to complete is just the original token.
1103 path = g.cwd;
1104 }
1105
1106 // This is a bit naughty, but necessary without reordering the loop
1107 // below. The loop terminates early, meaning that
1108 // the suggestions list is incomplete.
1109 // We only do this if the token is empty though.
1110 if (token.is_empty()) {
1111 Core::DirIterator suggested_files(path, Core::DirIterator::SkipDots);
1112 while (suggested_files.has_next()) {
1113 suggestions.append(suggested_files.next_path());
1114 }
1115 }
1116
1117 String completion;
1118
1119 bool seen_others = false;
1120 Core::DirIterator files(path, Core::DirIterator::SkipDots);
1121 while (files.has_next()) {
1122 auto file = files.next_path();
1123 if (file.starts_with(token)) {
1124 if (!token.is_empty())
1125 suggestions.append(file);
1126 if (completion.is_empty()) {
1127 completion = file; // Will only be set once.
1128 } else {
1129 editor.cut_mismatching_chars(completion, file, token.length());
1130 if (completion.is_empty()) // We cut everything off!
1131 return suggestions;
1132 seen_others = true;
1133 }
1134 }
1135 }
1136 if (completion.is_empty())
1137 return suggestions;
1138
1139 // If we have characters to add, add them.
1140 if (completion.length() > token.length())
1141 editor.insert(completion.substring(token.length(), completion.length() - token.length()));
1142 // If we have a single match and it's a directory, we add a slash. If it's
1143 // a regular file, we add a space, unless we already have one.
1144 if (!seen_others) {
1145 String file_path = String::format("%s/%s", path.characters(), completion.characters());
1146 struct stat program_status;
1147 int stat_error = stat(file_path.characters(), &program_status);
1148 if (!stat_error) {
1149 if (S_ISDIR(program_status.st_mode))
1150 editor.insert('/');
1151 else if (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ')
1152 editor.insert(' ');
1153 }
1154 }
1155
1156 return {}; // Return an empty vector
1157 };
1158
1159 signal(SIGINT, [](int) {
1160 g.was_interrupted = true;
1161 editor.interrupted();
1162 });
1163
1164 signal(SIGWINCH, [](int) {
1165 g.was_resized = true;
1166 editor.resized();
1167 });
1168
1169 signal(SIGHUP, [](int) {
1170 save_history();
1171 });
1172
1173 int rc = gethostname(g.hostname, sizeof(g.hostname));
1174 if (rc < 0)
1175 perror("gethostname");
1176 rc = ttyname_r(0, g.ttyname, sizeof(g.ttyname));
1177 if (rc < 0)
1178 perror("ttyname_r");
1179
1180 {
1181 auto* cwd = getcwd(nullptr, 0);
1182 if (cwd == nullptr)
1183 cwd = strdup("/");
1184 g.cwd = cwd;
1185 setenv("PWD", cwd, 1);
1186 free(cwd);
1187 }
1188
1189 {
1190 auto* pw = getpwuid(getuid());
1191 if (pw) {
1192 g.username = pw->pw_name;
1193 g.home = pw->pw_dir;
1194 setenv("HOME", pw->pw_dir, 1);
1195 }
1196 endpwent();
1197 }
1198
1199 if (argc > 2 && !strcmp(argv[1], "-c")) {
1200 dbgprintf("sh -c '%s'\n", argv[2]);
1201 run_command(argv[2]);
1202 return 0;
1203 }
1204
1205 if (argc == 2 && argv[1][0] != '-') {
1206 auto file = Core::File::construct(argv[1]);
1207 if (!file->open(Core::IODevice::ReadOnly)) {
1208 fprintf(stderr, "Failed to open %s: %s\n", file->filename().characters(), file->error_string());
1209 return 1;
1210 }
1211 for (;;) {
1212 auto line = file->read_line(4096);
1213 if (line.is_null())
1214 break;
1215 run_command(String::copy(line, Chomp));
1216 }
1217 return 0;
1218 }
1219
1220 g.directory_stack.append(g.cwd);
1221
1222 load_history();
1223 atexit(save_history);
1224
1225 cache_path();
1226
1227 for (;;) {
1228 auto line = editor.get_line(prompt());
1229 if (line.is_empty())
1230 continue;
1231 run_command(line);
1232 editor.add_to_history(line);
1233 }
1234
1235 return 0;
1236}