What you need to write your own shell

What you need to write your own shell

A breakdown of system and library functions used in creating shells

David John's photo
David John
·Nov 3, 2022·

10 min read

Simple Shell

In computing, a shell is a computer program that exposes an operating systems's services to a human user or to other programs. In general, operating system shells use either a command line interface(CLI) or graphical user interface(GUI), depending on a computer's role and particular operation. It is named a shell because it is the outermost layer around an operating system.

Here are key functions you'll need to create your own shell

access (sys call)

prototype: int access(const char *pathname, int mode);

headers needed: fcntl.h unistd.h

This function is used to figure out the following

  • if file exists: mode = F_OK

  • if file exists and is readable: mode = R_OK

  • if file exists and is writable: mode = W_OK

  • if file exists and is executable: mode = X_OK

The function returns 0 for true and -1 for false.

The R_OK W_OK X_OK modes can be combined using bitwise or (|). For example to check if a file is readable and executable, set mode to R_OK | X_OK

chdir (sys call)

prototype: int chdir(const char *path);

headers needed: unistd.h

This function changed the working directory. It returns 0 if success or -1 if it failed.

close (sys call)

This function closed a file descriptor.

prototype: int close(int fd);

headers need: unistd.h

It returns 0 if success or -1 if it failed.

closedir (lib call)

This function closes the directory stream associated with dirp.

prototype: int closedir(DIR *dirp);

headers needed: sys/types.h, dirent.h

It returns 0 if success or -1 if it failed.

execve (sys call)

This function executes the program program referred to by pathname.

prototype: int execve(const char *pathname, char *const argv[], char *const envp[]);

headers needed: unistd.h

It does not return anything on success, but returns -1 when it fails.

exit (lib call)

Exits the program with a status code.

prototype: void exit(int status);

headers needed: stdlib.h

Returns nothing.

_exit (sys call)

Like exit but does not call any function registered with atexit(3) or on_exit(3). Open stdio(3) streams are not flushed. Open file descriptors are not closed too.

prototype: void _exit(int status);

headers needed: unistd.h

Returns nothing.

fflush (lib call)

This function forces a write of all buffered data for the given output or update stream via the stream's underlying write function.

prototype: int fflush(FILE *stream);

headers needed: stdio.h

Returns 0 if successful. Otherwise, EOF is returned.

fork (sys call)

Create a child process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process

prototype: pid_t fork(void);

headers needed: sys/types.h unistd.h

example usage in a shell:

int child_process_id = fork();
if (child_process_id)
    // Fork returns a valid pid in the parent process.  Parent executes this.

    // wait for the child process to complete
    waitpid(child_process_id, ...);  // omitted extra args for brevity

    // child process finished!
else {
    // Fork returns 0 in the child process.  Child executes this.

    // new argv array for the child process
    const char *argv[] = {"arg1", "arg2", "arg3", NULL};

    // now start executing some other program
    exec("/path/to/a/program", argv);

free (lib call)

frees dynamic memory.

prototype: void free(void *ptr);

headers needed: stdlib.h

Returns nothing.

getcwd (lib call)

get current working directory.

prototype: char *getcwd(char *buf, size_t size);

headers needed: unistd.h

These functions return a null-terminated string containing an abso‐ lute pathname that is the current working directory of the calling process. The pathname is returned as the function result and via the argument buf, if present.

The getcwd() function copies an absolute pathname of the current working directory to the array pointed to by buf, which is of length size. If the length of the absolute pathname of the current working directory, including the terminating null byte, exceeds size bytes, NULL is returned. Your application should check for this error, and allocate a larger buffer if necessary.

getline (line call)

Reads an entire line from stream, storing the address of the buffer containing the text into *lineptr. The buffer is null-terminated and includes the newline character, if one was found.

prototype: ssize_t getline(char **lineptr, size_t *n, FILE *stream);

headers needed: stdlib.h

If *lineptr is set to NULL and *n is set to 0 before the call, then getline() will allocate a buffer for storing the line. This buffer should be freed by the user program even if getline() failed.

Alternativaly, before calling getline(), *lineptr can contain a pointer to a malloced buffer of *n bytes. If the buffer is t=not large enough to hold the line, getline() resizes it with realloc, updating *lineptr and *n as necessary.

On success, getline() returns the number of characters read, excluding the terminating null byte (\0). It returns -1 if it failed.

getpid (sys call)

Returns the process ID(PID) of the calling process.

prototype: pid_t getpid(void);

headers needed: sys/types.h unistd.h

isatty (lib call)

Test is a file descriptor refers to a terminal.

prototype: int isatty(int fd);

headers needed: unistd.h

Returns a boolean. 1 means true, while 0 means false.

kill (sys call)

Send a signal to a process.

prototype: int kill(pid_t pid, int sig);

headers needed:sys/types.h signal.h

here are conditions determining how kill() works

  • if pid is positive, then then sig is sent to the process with the ID specified by pid.

  • if pid equals 0, then sig is sent to every process in the process group of the calling process.

  • if pid equals -1, then sig is semt to every process for which the calling process has permission to send signals, except for the process 1 (init).

  • if pid is less than -1, then sig is sent to every process in the process group whose ID is -pid

  • if sig is 0, then no signal is sent, but the existence and permission checks are still performed.

On success (at least one signal was sent), zero is returned. On error, -1 is returned.

malloc(lib call)

Dynamically allocates size bytes and returns a pointer to the allocated memory. If size is 0, then malloc returns either NULL, or a unique pointer value that can later be successfully passed to free().

prototype: void *malloc(size_t size);

headers needed: stdlib.h

open (sys call)

Open and possibly create a file.


  • int open(const char *pathname, int flags);

  • int open(const char *pathname, int flags, mode_t mode);

headers needed: sys/types.h sys/stat.h fcntl.h

This function open the file specified by pathname. If the specified file does not exist, it may optionally (if O_CREAT is specified in flags) be created.

It returns a file descriptor - a small, nonnegative integer. If open() failed, it returns -1.

opendir (lib call)

Open a directory.

prototype: DIR *opendir(const char *name);

headers needed: sys/types.h dirent.h

Returns a pointer to the directory stream corresponding to the directory name. On error, NULL is returned.

perror (lib call)

Print a system error message.

prototype: void perror(const char *s);

headers needed: stdio.h errno.h

This function produces a message on standard error describing the last error encountered during a call to a system or library function.

First (if s is not NULL and *s is not a null byte ('\0')), s is printed, followed by a colon and a blank. Then an error message corresponding to the current value of errno and a newline.

Returns nothing.

read (sys call)

read from a file descriptor. read() attempts to read up to count bytes from file descriptor fd in the buffer starting at buf.

prototype: ssize_t read(int fd, void *buf, size_t count);

headers needed: unistd.h

Returns the number of bytes read or -1 if there was an error.

readdir (lib call)

reads a directory.

prototype: struct dirent *readdir(DIR *dirp);

headers needed: dirent.h

the returned structure

struct dirent
    ino_t d_ino; /* Inode number */
    off_t d_off; /* location of this file in directory stream */
    unsigned short d_reclen; /* length of this record */
    unsigned char d_type; /* Type of file; see types of files below */
    char d_name[256]; /* filename */

types of files

File TypeDescription
DT_BLKThis is a block device
DT_CHRThis is a character device
DT_DIRThis is a directory
DF_FIFOThis is a named pipe (FIFO)
DT_LNKThis is a symbolic link
DT_REGThis is a regular file
DT_SOCKThis is a UNIX domain socket
DT_UNKNOWNThe file type could not be determined

The readdir() function returns a pointer to a dirent structure representing the next directory entry in the directory stream pointerd to by dirp. It returns NULL on reaching the end of the directory stream or if any error occurred. To distinguish end of stream from an error, set errno to zero before calling readdir() and then check the value of errno if NULL is returned.

signal (sys call)

registers signal handlers.

typedef void (*sighandler_t)(int);

prototype: sighandler_t signal(int signum, sighandler_t handler);

headers needed: signal.h

signal() sets the disposition of the signal signum to handler, which is either SIG_DFL(default action), SIG_IGN(ignore signal), or the address of a programmer-defined function (a "signal handler").

The signals SIG and SIGSTOP cannot be caught or ignored.

It returns the previous signal handler or SIG_ERR on error.

stat (sys call)

Return information about a file, in the buffer pointed to by statbuf.

struct stat
    dev_t st_dev; /* ID of the device containing file */
    ino_t st_ino; /* Inode number */
    mode_t st_mode; /* File type and mode */
    nlink_t st_nlink; /* Number of hard links */
    uid_t st_uid; /* User ID of owner */
    gid_t st_gid; /* Group ID of owner */
    dev_t st_rdev; /* Device ID (if special file) */
    off_t st_size; /* Total size, in bytes */
    blksize_t st_blksize; /* Block size for filesystem I/O */
    blkcnt_t st_blocks; /* Number of 512B blocks allocated */
    struct timespec st_atim; /* Time of last access */
    struct timespec st_mtim; /* Time of last modification */
    struct timespec st_blocks; /* Time of last status change */

prototype: int stat(const char *pathname, struct sta *statbuf);

headers needed: sys/types.h sys/stat.h unistd.h

Returns 0 on success. Otherwise, it returns -1.

lstat (sys call)

lstat() is identical to stat(), except that is pathname is a symbolic link, then it returns information about the link itself, not the file the link refers to.

prototype: int lstat(const char *pathname, struct sta *statbuf);

headers needed: sys/types.h sys/stat.h unistd.h

Returns 0 on success. Otherwise, it returns -1.

fstat (sys call)

fstat() is identical to stat(), except that the file about which information is to be retrieved is specified by the file descriptor fd.

prototype: int fstat(int fd, struct sta *statbuf);

headers needed: sys/types.h sys/stat.h unistd.h

Returns 0 on success. Otherwise, it returns -1.

strtok (lib call)

Extract tokens from a string.

prototype: char *strtok(char *str, const char *delim)

headers required: string.h

The strtok() function breaks a string into a sequence of zero or more nonempty tokens. On the first call to strtok(), the string to be parsed should be specified in str. In each subsequent call that should parse the same string, str must be NULL.

Return a pointer to the next token, or NULL if there are no more tokens.

waitpid (sys call)

Wait for a process to change state.

prototype: pid_t waitpid(pid_t pid, int *wstatus, int options);

headers needed: sys/types.h sys/wait.h

This system call suspends execution of the calling thread until a child specified by pid argument has changed state.

value of pidmeaning
< -1wait for any child process whose group ID is equal to the absolute value of pid
-1wait for any child process
0wait for any child process whose group ID is equal to that of the calling process at the time of the call to waitpid()
> 0wait for the child whose process ID is equal to the value of pid
value of optionsbehaviour
WNOHANGreturn immediately if no child has exited
WUNTRACEDalso return if a child has stopped
WCONTINUEDalso return if a stopped child has been resumed by delivery of SIGCONT

The value of options can be an OR of zero or more of the above constants.

If wsatus is not NULL, the function stores status information in the int to which it points.

On success, it returns the process ID of the child process whose state has changed. On error, -1 is returned.

Share this