Lab 2: Shell
Introduction
The shell is the main command-line interface between a user and the operating system, and it is an essential part of the daily lives of computer scientists, software engineers, system administrators, and such. It makes heavy use of many OS features. In this lab, you will build a simplified version of the Unix shell called the New Yet Usable SHell, or nyush for short.
Please review the first lecture of MIT’s The Missing Semester of Your CS Education if you are not familiar with the shell.
Objectives
Through this lab, you will:
Familiarize yourself with the Linux programming environment and the shell, of course.
Learn how to write an interactive command-line program.
Learn how processes are created, destroyed, and managed.
Learn how to handle signals and I/O redirection.
Get a better understanding of the OS and system calls.
Be a better C programmer and be better prepared for your future technical job interviews. In particular, the string parsing skill that you will practice in this lab is desired in many interview questions.
Overview
The shell is essentially a command-line interpreter. It works as follows:
1. It prompts you to enter a command.
2. It interprets the command you entered.
If you entered a built-in command (e.g., cd ), then the shell runs that command.
If you entered an external program (e.g., /bin/ls ), or multiple programs connected through pipes (e.g., ls -l | less ), then the shell creates child processes, executes these programs, and waits for all these processes to either terminate or be suspended.
If you entered something wrong, then the shell prints an error message.
3. Rinse and repeat until you press Ctrl-D to close STDIN or enter the built-in command exit , at which point the shell exits.
Specifications
Your shell should follow these specifications carefully. These specifications may be slight different from the default Linux shell ( bash ) for simplicity.
The prompt
The prompt is what the shell prints before waiting for you to enter a command. In this lab, your prompt must have exactly the following format:
An opening bracket [ .
The word nyush .
A whitespace.
The basename of the current working directory.
A closing bracket ] .
A dollar sign .<br>Anotherwhitespace.<br>Forexample,ifyouarein/home/abc123/2250/lab2,thenthepromptshouldbe:<br>[nyushlab2] █
If you are in the root directory ( / ), then the prompt should be:
[nyush /]█<br>Notethatthefinal█characterintheseexamplesrepresentsyourcursor;youshouldnotprintthatcharacterinyourshellprompt.<br>KeepinmindthatSTDOUTisline−bufferedbydefault.Therefore,don’tforgettoflushSTDOUTimmediatelyafteryouprinttheprompt.Otherwise,yourprogrammaynotworkcorrectlywiththeautograder.<br><br><br><br><br>Thecommand<br>Ineachiteration,theuserinputsacommandterminatedbythe“enter”key(i.e.,newline).Acommandmaycontainmultipleprogramsseparatedbythepipe(∣)symbol.Avalidcommandmustsatisfythefollowingrequirements:<br>Iftherearemultipleprogramsinacommand,onlythefirstprogrammayredirectitsinput(using<),andonlythelastprogrammayredirectitsoutput(using>or>>).Ifthereisonlyoneprograminacommand,itmayredirectbothinputandoutput.<br>Ineachcommand,therearenomorethanoneinputredirectionandoneoutputredirection.<br>Built−incommands(e.g.,cd)cannotbeI/Oredirectedorpiped.<br>Forsimplicity,ourtestcaseshavethefollowingassumptions:<br>Eachcommandhasnomorethan1000characters.<br>Thereisalwaysasinglespaceseparatingfilenames,arguments,andthepipeandredirectionsymbols(∣,<,>,>>).<br>Therearenospaceswithinafilenameoranargument.<br>Foryourreference,hereisthegrammarforvalidcommands(don’tworryifyoucan’tunderstandit;justlookattheexamplesbelow):<br><br><br><br>[command]:="";or:=[cd][arg];or:=[exit];or:=[fg][arg];or:=[jobs];or:=[cmd]′<′[filename][recursive];or:=[cmd]′<′[filename][terminate];or:=[cmd][recursive];or:=[cmd][terminate]′<′[filename];or:=[cmd][terminate].[recursive]:=′∣′[cmd][recursive];or:=′∣′[cmd][terminate].[terminate]:="";or:=′>′[filename];or:=′>>′[filename].[cmd]:=[cmdname][arg]∗[cmdname]:=Astringwithoutanyspace,tab,>(ASCII62),<(ASCII60),∣(ASCII124),∗(ASCII42),!(ASCII33),‘(ASCII96),′(ASCII39),nor"(ASCII34)characters.Besides,thecmdnameisnotcd,exit,fg,jobs.[arg]:=Astringwithoutanyspace,tab,>(ASCII62),<(ASCII60),∣(ASCII124),∗(ASCII42),!(ASCII33),‘(ASCII96),′(ASCII39),nor"(ASCII34)characters.[filename]:=Astringwithoutanyspace,tab,>(ASCII62),<(ASCII60),∣(ASCII124),∗(ASCII42),!(ASCII33),‘(ASCII96),′(ASCII39),nor"(ASCII34)characters.<br><br><br><br><br>Herearesomeexamplesofvalidcommands:<br>Ablankline.<br>/usr/bin/ls−a−l<br>catshell.c∣grepmain∣less<br>cat<input.txt<br>cat>output.txt<br>cat>>output.txt<br>cat<input.txt>output.txt<br>cat<input.txt>>output.txt<br>cat>output.txt<input.txt<br>cat>>output.txt<input.txt<br>cat<input.txt∣cat>output.txt<br>cat<input.txt∣cat∣cat>>output.txt<br>Herearesomeexamplesofinvalidcommands:<br>cat<<br>cat><br>cat∣<br>∣cat<br>cat<<file.txt<br>cat<file.txt<file2.txt<br>cat<file.txtfile2.txt<br>cat>file.txt>file2.txt<br>cat>file.txt>>file2.txt<br>cat>file.txtfile2.txt<br>cat>file.txt∣cat<br>cat∣cat<file.txt<br>cd/>file.txt<br>Ifthereisanyerrorinparsingthecommand,thenyourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invalidcommand<br>Notethatthereshouldbeanewlineattheendoftheerrormessage.Forexample:<br>[nyushlab2] cat <
Error: invalid command
[nyush lab2]█<br>(Again,thefinal█characterrepresentsyourcursor.)<br>Locatingprograms<br>Youcanspecifyaprogrambyeitheranabsolutepath,arelativepath,orbasenameonly.<br>1.Anabsolutepathbeginswithaslash(/).Iftheuserspecifiesanabsolutepath,thenyourshellmustruntheprogramatthatlocation.<br>2.Arelativepathcontains,butnotbeginswith,aslash(/).Iftheuserspecifiesarelativepath,thenyourshellshouldlocatetheprogrambyfollowingthepathfromthecurrentworkingdirectory.Forexample,dir1/dir2/programisequivalentto./dir1/dir2/program.<br>3.Otherwise,iftheuserspecifiesonlythebasenamewithoutanyslash(/),thenyourshellmustsearchfortheprogramunder/usr/bin.Forexample,whentheusertypesls,thenyourshellshouldtry/usr/bin/ls.Ifthatfails,itisanerror.Inthiscase,yourshellshouldnotsearchthecurrentworkingdirectory.Forexample,supposethereisaprogramnamedhellointhecurrentworkingdirectory.Enteringhelloshouldresultinanerror,whereas./hellorunstheprogram.<br>Inanycase,iftheprogramcannotbelocated,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invalidprogram<br><br><br><br><br>Processterminationandsuspension<br>Aftercreatingtheprocesses,yourshellmustwaituntilalltheprocesseshavestoppedrunning—eitherterminatedorsuspended.Then,yourshellshouldprompttheuserforthenextcommand.<br>Yourshellmustnotleaveanyzombiesinthesystemwhenitisreadytoreadthenextcommandfromtheuser.<br>Signalhandling<br>IfauserpressesCtrl−CorCtrl−Z,theydon’texpecttoterminateorsuspendtheshell.Therefore,yourshellshouldignorethefollowingsignals:SIGINT,SIGQUIT,andSIGTSTP.Allothersignalsnotlistedhereshouldkeepthedefaultsignalhandlers.<br>Notethatonlytheshellitself,notthechildprocessescreatedbytheshell,shouldignorethesesignals.Forexample,<br>[nyushlab2] cat
^C
[nyush lab2] █<br>Here, the signal SIGINT generated by Ctrl-C terminates only the process cat , not the shell itself.<br>As a side note, if your shell ever hangs and you would like to kill the shell, you can still send it the SIGTERM or SIGKILL signal. To do so, you can connect to the running Docker container from another terminal window using:<br>docker exec -it 2250 bash<br>…and then kill your shell using:<br>[root@... 2250]# killall nyush<br>I/O redirection<br>Sometimes, a user would read the input to a program from a file rather than the keyboard, or send the output of a program to a file rather than the screen. Your shell should be able to redirect the standard input ( STDIN ) and the standard output ( STDOUT ). For simplicity, you are not required to redirect the standard error ( STDERR ).<br>Input redirection<br>Input redirection is achieved by a < symbol followed by a filename. For example:<br>[nyush lab2] cat < input.txt
If the file does not exist, your shell should print the following error message to STDERR and prompt for the next command.
Error: invalid file
Output redirection
Output redirection is achieved by > or >> followed by a filename. For example:
[nyush lab2]ls−l>output.txt<br>[nyushlab2] ls -l >> output.txt
If the file does not exist, a new file should be created. If the file already exists, redirecting with > should overwrite the file (after truncating it), whereas redirecting with >> should append to the existing file.
Pipe
A pipe ( | ) connects the standard output of the first program to the standard input of the second program. For example:
[nyush lab2]catshell.c∣wc−l<br>Theusermayinvokenprogramschainedthrough(n−1)pipes.Eachpipeconnectstheoutputoftheprogramimmediatelybeforethepipetotheinputoftheprogramimmediatelyafterthepipe.Forexample:<br>[nyushlab2] cat shell.c | grep main | less
Here, the output of cat shell.c is the input of grep main , and the output of grep main is the input of less .
Built-in commands
Every shell has a few built-in commands. When the user issues a command, the shell should first check if it is a built-in command. If so, it should not be executed like other programs.
In this lab, you will implement four built-in commands: cd , jobs , fg , and exit .
cd
This command changes the current working directory of the shell. It takes exactly one argument: the directory, which may be an absolute or relative path. For example:
[nyush lab2]
cd/usr/local<br>[nyushlocal] cd bin
[nyush bin]
█<br>Ifcdiscalledwith0or2+arguments,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invalidcommand<br>Ifthedirectorydoesnotexist,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invaliddirectory<br>jobs<br>ThiscommandprintsalistofcurrentlysuspendedjobstoSTDOUT,onejobperline.Eachlinehasthefollowingformat:[index]command.Forexample:<br>[nyushlab2] jobs
[1] ./hello
[2] /usr/bin/top -c
[3] cat > output.txt
[nyush lab2]
█<br>(Iftherearenocurrentlysuspendedjobs,thiscommandshouldprintnothing.)<br>Ajobisthewholecommand,includinganyargumentsandI/Oredirections.AjobmaybesuspendedbyCtrl−Z,theSIGTSTPsignal,ortheSIGSTOPsignal.Thislistissortedbythetimeeachjobissuspended(oldestfirst),andtheindexstartsfrom1.<br><br><br><br><br>NoteforWindowsusers<br>Ifyou’reusingrecentversionsofWindows,youmightneedtoenableLegacyConsolemodeinorderforCtrl−ZtobeproperlyhandledbyPowerShell.<br>Forsimplicity,wehavethefollowingassumptions:<br>Therearenomorethan1代写Lab2:ShellC/C++00suspendedjobsatonetime.<br>Therearenopipesinanysuspendedjobs.<br>Theonlywaytoresumeasuspendedjobisbyusingthefgcommand(seebelow).Wewillnottrytoresumeorterminateasuspendedjobbyothermeans.WewillnottrytopressCtrl−CorCtrl−Dwhiletherearesuspendedjobs.<br>Youdon’tneedtoworryabout“processgroups.”(Ifyoudon’tknowwhatprocessgroupsare,don’tworry.)<br>Thejobscommandtakesnoarguments.Ifitiscalledwithanyarguments,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.Error:invalidcommand<br>fg<index><br>Thiscommandresumesajobintheforeground.Ittakesexactlyoneargument:thejobindex,whichisthenumberinsidethebracketprintedbythejobscommand.Forexample:<br>[nyushlab2] jobs
[1] ./hello
[2] /usr/bin/top -c
[3] cat > output.txt
[nyush lab2]
fg2<br>Thelastcommandwouldresume/usr/bin/top−cintheforeground.Notethatthejobindexofcat>output.txtwouldbecome2asaresult.Shouldthejob/usr/bin/top−cbesuspendedagain,itwouldbeinsertedtotheendofthejoblist:<br>[nyushlab2] jobs
[1] ./hello
[2] cat > output.txt
[3] /usr/bin/top -c
[nyush lab2]
█<br>Iffgiscalledwith0or2+arguments,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invalidcommand<br>Ifthejobindexdoesnotexistinthelistofcurrentlysuspendedjobs,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invalidjob<br>exit<br>Thiscommandterminatesyourshell.However,iftherearecurrentlysuspendedjobs,yourshellshouldnotterminate.Instead,itshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:therearesuspendedjobs<br>Theexitcommandtakesnoarguments.Ifitiscalledwithanyarguments,yourshellshouldprintthefollowingerrormessagetoSTDERRandpromptforthenextcommand.<br>Error:invalidcommand<br>NotethatiftheSTDINofyourshellisclosed(e.g.,bypressingCtrl−Dattheprompt),yourshellshouldterminateregardlessofwhethertherearesuspendedjobs.<br><br><br>Compilation<br>Wewillgradeyoursubmissioninanx8664RockyLinux8containeronGradescope.Wewillcompileyourprogramusinggcc12.1.1withtheC17standardandGNUextensions.<br>YoumustprovideaMakefile,andbyrunningmake,itshouldgenerateanexecutablefilenamednyushinthecurrentworkingdirectory.(RefertoLab1foranexampleoftheMakefile.)<br>Yourprogrammustnotcallthesystem()functionorexecute/bin/sh.Otherwise,whatisthewholepointofthislab?<br>Evaluation<br>Beatupyourowncodeextensively.Betteryet,eatyourowndogfood.Iwouldhappilyusenyushasmymainshell(atleastforthedurationofthislab),sowhywouldn’tyou?<br>Weareprovidingasampleautograderwithafewtestcases.PleaseextracttheminsidetheDockercontainerandfollowtheinstructionsintheREADMEfile.(RefertoLab1forhowtoextracta.tar.xzfile.)<br>Notethatthesetestcasesarenotexhaustive.ThetestcasesonGradescopearedifferentfromtheonesprovidedandwillnotbedisclosed.Donottrytohackorexploittheautograder.<br>Submission<br>Youmustsubmita.ziparchivecontainingallfilesneededtocompilenyushintherootofthearchive.YoucancreatethearchivefilewiththefollowingcommandintheDockercontainer:<br> zip nyush.zip Makefile *.h *.c
(Omit *.h if you don’t have header files.)
Note that other file formats (e.g., rar ) will not be accepted.
You need to upload the .zip archive to Gradescope. If you need to acknowledge any influences per our academic integrity policy, write them as comments in your source code.
Rubric
The total of this lab is 100 points, mapped to 15% of your final grade of this course.
Compile successfully and can print the correct prompt. (40 points)
Process creation and termination. (20 points)
Simple built-in commands ( cd and exit ) and error handling. (10 points)
Input and output redirection. (10 points)
Pipes. (10 points)
Handling suspended jobs ( jobs and fg ). (10 points)
You will get 0 points for this lab if you call the system() function or execute /bin/sh.
Please make sure that your shell prompt and all error messages are exactly as specified in this document. Any discrepancy may lead to point deductions.
Tips
Don’t procrastinate! Don’t procrastinate!! Don’t procrastinate!!!
This lab is significantly more challenging than Lab 1 and requires significant programming effort. Therefore, start as early as possible! Don’t wait until the last week.
How to get started?
Please review our academic integrity policy carefully before you start.
This lab requires you to write a complete system from scratch, so it may be daunting at first. Remember to get the basic functionality working first, and build up your shell step-by-step.
Here is how I would tackle this lab:
Milestone 1. Write a simple program that prints the prompt and flushes STDOUT . You may use the getcwd() system call to get the current working directory.
Milestone 2. Write a loop that repeatedly prints the prompt and gets the user input. You may use the getline() library function to obtain user input.
Milestone 3. Extend Milestone 2 to be able to run a simple program, such as ls . At this point, what you’ve written is already a shell! Conceptually, it might look like:
while (true) {// print prompt// read commandpid = fork();if (pid < 000) {// fork failed (this shouldn't happen)} else if (pid == 000) {// child (new process)execv(...); // or another variant// exec failed} else {// parentwaitpid(-111, ...);}}
Milestone 4. Extend Milestone 3 to run a program with arguments, such as ls -l .
Milestone 5. Handle simple built-in commands ( cd and exit ). You may use the chdir() system call to change the working directory.
Milestone 6. Handle output redirection, such as cat > output.txt .
Milestone 7. Handle input redirection, such as cat < input.txt .
Milestone 8. Run two programs with one pipe, such as cat | cat . The example code in man 2 pipe is very helpful.
Milestone 9. Handle multiple pipes, such as cat | cat | cat .
Milestone 10. Handle suspended jobs and related built-in commands ( jobs and fg ). Read man waitpid to see how to wait for a stopped child.
Some students may find handling suspended jobs easier than implementing pipes, so feel free to rearrange these milestones as you see fit.
Keep versions of your code! Use git or similar tools, but don’t make your repository public.
Useful system calls
Your shell will make many system calls. Here are a few that you may find useful.
Process management: access() , fork() , execv() (or other variants), waitpid() .
I/O redirection and pipes: dup2() , creat() , open() , close() , pipe() .
Signal handling: signal() .
Built-in commands: chdir() , getcwd() , kill() .
You might not need to use all of them, and you are free to use other system calls not mentioned above.
Check the return values (and possibly, errno ) of all system calls and library function calls from the very beginning of your work! This will often catch errors early, and it is a good programming practice.
Reading man pages
Man pages are of vital importance for programmers working on Linux and such. It’s a treasure trove of information.
Man pages are divided into sections. Please see man man for the description of each section. In particular, Section 2 contains system calls. You will need to look them up a lot in this lab.
Sometimes, you need to specify the section number explicitly. For example, man kill shows the kill command in Section 1 by default. If you need to look up the kill() system call, you need to invoke man 2 kill .
Parsing the command line
You might find writing the command parser troublesome. Don’t be frustrated. You are not alone. However, it is an essential skill for any programmer, and it often appears in software engineer interviews. Once you get through it, you will never be afraid of it again.
I personally find the strtok_r() function extremely helpful. You don’t have to use it, but why not give it a try?
In gdb, how to debug the child process after fork()?
By default, when a program forks, gdb will continue to debug the parent process, and the child process will run unimpeded.
If you want to follow the child process instead of the parent process, use the command set follow-fork-mode child .
If you want to debug both the parent and child processes, use the command set detach-on-fork off . Then, you can use info inferiors to show all processes and use inferior to switch to another process.
I’m still confused about pipes. Any more hints?
If you can already support I/O redirection for a single process, adding support for pipes shouldn’t be too difficult since a pipe is just two I/O redirections. Start by reading the example code in man 3 pipe and writing a simple program to see how pipe works.
Here are some additional hints:
Make sure a single pipe works before trying multiple pipes.
To support a single pipe, the parent (shell) process should create the pipe and fork two children. The first child redirects its output to the write end of the pipe, and the second child redirects its input from the read end of the pipe. In other words, instead of opening a file for redirection, use the file descriptors returned by pipe() .
Both children should run concurrently. The parent (shell) process should wait for both children. In other words, a child shouldn’t wait for another child.
Make sure to close all unused file descriptors, otherwise your child process may not terminate. In the above case, the parent holds two ends of the pipe, and after fork() , each child holds two ends of the pipe. So altogether you need to close six file descriptors. The parent should close them before calling waitpid() , and the child should close them before calling execv() .
Supporting multiple pipes is similar. For N processes, your shell should create N-1 pipes and fork N children. You can either write a loop or use recursion to do that.
I’m still confused about suspended jobs. Any more hints?
To support suspended jobs, your shell should call waitpid() with the WUNTRACED option and check the returned status with WIFSTOPPED() . If that’s true, add the job to the list of suspended jobs.
To resume a job, just kill() it with SIGCONT .
I don’t want to reënter gdb when I press Ctrl-Z. Is it possible?
You can always use c to continue the program, or you can use the command handle SIGTSTP nostop noprint to prevent gdb from handling the signal.
I hate the command line! Can I debug in VS Code?
First, I recommend leaving your comfort zone and embracing the command line. For many of you, it will be an essential skill in your career.
However, if you can’t live without a GUI, you can follow these steps to attach VS Code to a running container:
1. Install the Dev Containers extension in VS Code.
2. Start the container in your OS as usual using the docker run command.
3. In VS Code, press Ctrl-Shift-P (Windows) or ⇧⌘P (Mac) and select Dev Containers: Attach to Running Container... .
4. Select cs202 from the pop-up.
5. Now, you have connected your VS Code to the container. Remember to install useful extensions inside the container, such as the C/C++ extension, since it is a remote environment.
6. You can configure gdb in VS Code.
7. To switch between processes in VS Code, add these lines to launch.json .
WX:codehelp