1

I have come up with a code for a simple shell in C (with some help from ChatGPT). However, for the first part of my endless list of to-dos, I can't make my shell give me the same error code as echo "qwerty" | /bin/sh which is /bin/sh: 1: qwerty not found Something to do with my execute_command() function but can't figure it out on my own. After compilation, my shell should have error message like ./hsh: 1: qwerty: not found So far, I have been able to print out ...qwerty: not found but not the name of the custom shell or the line number.

#define SHELL_H

#define MAX_NUM_ARGS 10


/* Standard Headers */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>


/* Function Prototypes */
void parse_command(char *command, char *args[]);
void execute_command(char *args[]);


#endif /* #ifndef SHELL_H */

void execute_command(char *args[])
{
    int status;
    pid_t pid;
    pid_t ppid = getppid();
    char cmd[100], shell[100];
    FILE *fp;

    snprintf(cmd, sizeof(cmd), "ps -p %d -o comm=", ppid);

    fp = popen(cmd, "r");

    if (fp == NULL)
        perror("Failed to execute command\n");

    if (fgets(shell, sizeof(shell), fp) == NULL)
        perror("Failed to read output\n");
    pclose(fp);

    pid = fork();
    if (pid == 0)
    {
        execvp(args[0], args);
        printf("%s: %d: %s: not found\n", shell, __LINE__, args[0]);
        exit(1);
    }
    else if (pid > 0)
    {
        wait(&status);
    }
    else
    {
        perror("Error: failed to fork\n");
        exit(1);
    }
}

void parse_command(char *command, char *args[])
{
    char *token = strtok(command, " ");
    int i = 0;

    while (token != NULL && i < MAX_NUM_ARGS)
    {
        args[i] = token;
        token = strtok(NULL, " ");
        i++;
    }
    args[i] = NULL;
}

int main(void)
{
    char *command = NULL;
    size_t n = 0;
    char *args[MAX_NUM_ARGS + 1];

    /*
     * function isatty() is used to check if input is
     * coming from a pipe or the terminal. Read man isatty()
     */
    if (!isatty(STDIN_FILENO))
    {
        if (getline(&command, &n, stdin) == -1)
        {
            perror("Error: getline() failed\n");
            return (1);
        }
        command[strcspn(command, "\n")] = '\0';
        parse_command(command, args);
        execute_command(args);
        free(command);
        return (0);
    }
    while (1)
    {
        printf(">> ");
        if (getline(&command, &n, stdin) == -1)
        {
            perror("Error: getline() failed\n");
            break;
        }
        command[strcspn(command, "\n")] = '\0';
        parse_command(command, args);

        if (strcmp(args[0], "exit") == 0)
        {
            break;
        }
        execute_command(args);
    }
    free(command);
    return (0);
}
Craig Estey
  • 30,627
  • 4
  • 24
  • 48

1 Answers1

3

A few issues ...

  1. You don't need to do popen on ps to get the name of the shell. It is passed to main as argv[0]
  2. __LINE__ is the current line of the source code file for your shell that comes from the C preprocessor.
  3. It is not the line number of the command file that your shell is reading.
  4. You have to keep track of that yourself with a variable that you increment (e.g. line_number)

Here is the corrected code:

#ifndef SHELL_H
#define SHELL_H

#define MAX_NUM_ARGS 10

/* Standard Headers */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

/* Function Prototypes */
void parse_command(char *command, char *args[]);
void execute_command(char *args[]);

#endif /* #ifndef SHELL_H */

#if 1
char *shell;
int line_number;
#endif

void
execute_command(char *args[])
{
    int status;
    pid_t pid;
    pid_t ppid = getppid();
#if 0
    char cmd[100], shell[100];
#else
    char cmd[100];
#endif

#if 0
    FILE *fp;

    snprintf(cmd, sizeof(cmd), "ps -p %d -o comm=", ppid);

    fp = popen(cmd, "r");

    if (fp == NULL)
        perror("Failed to execute command\n");

    if (fgets(shell, sizeof(shell), fp) == NULL)
        perror("Failed to read output\n");
    pclose(fp);
#endif

#if 1
    ++line_number;
#endif

    pid = fork();
    if (pid == 0) {
        execvp(args[0], args);
#if 0
        printf("%s: %d: %s: not found\n", shell, __LINE__, args[0]);
#else
        printf("%s: %d: %s: not found\n", shell, line_number, args[0]);
#endif
        exit(1);
    }
    else if (pid > 0) {
        wait(&status);
    }
    else {
        perror("Error: failed to fork\n");
        exit(1);
    }
}

void
parse_command(char *command, char *args[])
{
    char *token = strtok(command, " ");
    int i = 0;

    while (token != NULL && i < MAX_NUM_ARGS) {
        args[i] = token;
        token = strtok(NULL, " ");
        i++;
    }
    args[i] = NULL;
}

#if 0
int
main(void)
#else
int
main(int argc,char **argv)
#endif
{
    char *command = NULL;
    size_t n = 0;
    char *args[MAX_NUM_ARGS + 1];

#if 1
    shell = *argv;
#endif

    /*
     * function isatty() is used to check if input is
     * coming from a pipe or the terminal. Read man isatty()
     */
    if (!isatty(STDIN_FILENO)) {
        if (getline(&command, &n, stdin) == -1) {
            perror("Error: getline() failed\n");
            return (1);
        }
        command[strcspn(command, "\n")] = '\0';
        parse_command(command, args);
        execute_command(args);
        free(command);
        return (0);
    }
    while (1) {
        printf(">> ");
        if (getline(&command, &n, stdin) == -1) {
            perror("Error: getline() failed\n");
            break;
        }
        command[strcspn(command, "\n")] = '\0';
        parse_command(command, args);

        if (strcmp(args[0], "exit") == 0) {
            break;
        }
        execute_command(args);
    }
    free(command);
    return (0);
}

In the code above, I've used cpp conditionals to denote old vs. new code:

#if 0
// old code
#else
// new code
#endif

#if 1
// new code
#endif

Note: this can be cleaned up by running the file through unifdef -k


UPDATE:

Why cpp conditionals though? This is supposed to be C. I really do not know if c and cpp are intertwined.

cpp stands for "C preprocessor" and not C++. Do man cpp.

Herein, it is used to "comment out" [or in] blocks of code (vs. using /* and */).

The C compiler is cc et. al. (e.g. gcc, clang) and most invoke the separate cpp command to do preprocessing.

Likewise for c++ (the C++ compiler) et. al. (e.g. g++, clang++).

clang/clang++ have merged the preprocessing step into the compiler itself, so it doesn't use cpp but the effect is mostly the same.

Plus isn't argv[0] going to be echo for echo "qwerty") | ./hsh – Akintola Oluwaseyi Alex

No, "qwerty" is merely the stdin input for the ./hsh. It has nothing to do with argv

For:

./hsh quick brown fox

We get an argc of 4 and argv values of:

argv[0] = ./hsh
argv[1] = quick
argv[2] = brown
argv[3] = fox
argv[4] = NULL

Similarly, the echo "qwerty" command has its own argc/argv (which your program can not get access to). argc would be 2 and argv:

argv[0] = echo
argv[1] = qwerty
argv[2] = NULL

If you'd like to see an example of a working shell that does pipes, see my answer: fd leak, custom Shell

A recommendation:

Because you're having some trouble with some basic concepts, before you tackle writing a shell, I'd do simpler programs first (e.g):

  1. The "hello world" program.
  2. A program the prints out the contents of argv
  3. Programs that input values into an array and take the sum, average, min/max, and sort the array.
  4. etc, ...

See the bottom of my answer: What is the error in this code that checks if the linklist is a palindrome or not? for a list of resources I recommend.

Since you're trying to learn C, I might look at cs50

And ... I wouldn't use chatGPT if I were you. I played with it to see how good it was and it took me 15 seconds to completely confuse it.

Craig Estey
  • 30,627
  • 4
  • 24
  • 48
  • Why ```cpp``` conditionals though? This is supposed to be C. I really do not know if `c` and `cpp` are intertwined. Plus isn't argv[0] going to be `echo` for `echo "qwerty") | ./hsh` – Akintola Oluwaseyi Alex Apr 23 '23 at 22:34
  • @ikegami OP doesn't want the _parent_ shell (e.g. `bash`). He wants _his_ shell as mentioned by his: _After compilation, my shell should have error message like_ `./hsh: 1: qwerty: not found` – Craig Estey Apr 23 '23 at 22:47
  • @AkintolaOluwaseyiAlex: The program `echo` will have `argv[0]` as `echo`; the program `./hsh` will have `./hsh` as `argv[0]`. Each program has `argv[0]` set to its own name. (You can, if you work hard, launch a program with a misleading name. For example, `execl("/bin/ls", "elephant", "-l", (char *)0);` will execute the `ls` program (assuming it can be found in `/bin` and isn't only found in `/usr/bin`), with the name `"elephant"`. If you pipe the output to `less` (or `more`) in one window and run `ps` in another, you'll see `elephant -l` running. But that's aconventional.) – Jonathan Leffler Apr 24 '23 at 02:26
  • @CraigEstey, I know a little about argc and argv, but apparently, my knowledge is insufficient. And as Jonathan pointed out, I think I understand better now. – Akintola Oluwaseyi Alex Apr 24 '23 at 06:02