[Linux Elementary] Application of Process Replacement – Implementation of Simple Command Line Interpreter

hello, readers, how are you
Series of columns: [Linux Elementary]
The content of this article: use code to implement a simple command line interpreter, the functions include: print output prompt, get user input, string cutting, execute command, color prompt under ls command, cd, echo ;
Author brief introduction: A new captain of Computer Ocean, please give me your advice ( ? ) –

Article directory

  • foreword
  • 1. Review the application of execvp
  • Second, the printout prompt
  • 3. Get user input
  • Four, string cutting
  • 5. Executing orders
  • Six, ls add color
  • Seven, realize cd
  • 8. Realize echo
  • epilogue

Foreword

This article is based on the basic knowledge of process replacement after learning. If you have a small partner who is not clear about the knowledge of process replacement, you can refer to this article of mine: [Linux Elementary] Process program replacement | Introduction, principle, function , application & multi-file compilation of makefile tools

1. Review the application of execvp

In our course of learning the basics of program substitution, we have come across the execvp function:

code example

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char* argv[])
{<!-- -->
    printf("process is running...\
");
    pid_t id = fork();
    assert(id != -1);

    if (id == 0)
    {<!-- -->

        sleep(1);
        // The execvp transfer method is as follows
        // ./exec ls -a -l -> "./exec" "ls" "-a" "-l"
        execvp(argv[1], & argv[1]);

        exit(1); //must fail
    }

    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) printf("wait success: exit code: %d, sig: %d\
", (status >> 8) & amp; 0xFF, status & amp; 0x7F);
}

It realizes the command of replacing the shell with our own program, so if we can further simplify the way of running, can’t it become a simple shell?

Next, I will take you to combine relevant knowledge to make a simple command line interpreter. First, we create the file myshell.c and Makefile, and write the code in it.

2. Printout prompt

  • Makefile writing

  • Myshell.c file writing
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //process replacement
#include <sys/types.h> //process waiting
#include <sys/wait.h>

int main()
{<!-- -->
    // output prompt
    printf("username@hostname current path#");
    fflush(stdout); //flush the output buffer
    sleep(10);
}

After completion, a prompt can be output, and other instructions can be entered after the prompt.

3. Get user input

#include <stdio.h>
#include <stdlib.h> //fgets
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h> //Assertion

#define NUM 1024

char lineCommand[NUM];

int main()
{<!-- -->
    // output prompt
    printf("username@hostname current path#");
    fflush(stdout);

    // Get user input, when the input is complete, enter \

    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
    assert(s != NULL);
    (void)s;
    printf("test : %s\
", lineCommand);
}

By running, we found that after entering the command and pressing Enter, it also reads our carriage return, resulting in a blank line when printing, we need to remove the /n at the end of the string.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024

char lineCommand[NUM];

int main()
{<!-- -->
    // output prompt
    printf("username@hostname current path#");
    fflush(stdout);

    // Get user input, when the input is complete, enter \

    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//#include <string.h>
    assert(s != NULL);//#include <assert.h>
    (void)s;
    // Clear the last \
 , abcd\

    lineCommand[strlen(lineCommand) - 1] = 0; // Change the last \
 to 0
    printf("test : %s\
", lineCommand);
}

We found that the blank lines disappeared!

4. String cutting

In practical applications, we need to cut the complete string to make it into substrings one by one, which is convenient for the computer to read (“ls -a -l -i” -> “ls” “-a” “- l” “-i”), we can create pointer array, and call strtok interface in C language to complete string cutting.

#include <stdio.h>
#include <string.h> //strtok
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64 //New macro

char lineCommand[NUM];
char *myargv[OPT_NUM]; //array of pointers

int main()
{<!-- -->
    // output prompt
    printf("username@hostname current path#");
    fflush(stdout);

    // Get user input, when inputting, enter\

    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
    assert(s != NULL);
    (void)s;
    // Clear the last \
 , abcd\

    lineCommand[strlen(lineCommand) - 1] = 0; // ?
    //printf("test : %s\
", lineCommand);

    // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
    // String cutting
    myargv[0] = strtok(lineCommand, " ");//For the first split, you need to pass in string + delimiter
    int i = 1;

    // If there is no substring, strtok will return NULL, myargv[end] = NULL
    while (myargv[i + + ] = strtok(NULL, " "));//For the second split, NULL + delimiter should be passed in
    
    // Whether the test is successful, conditional compilation - can be understood in conjunction with the following, it does not affect here
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i ++ )
        {<!-- -->
            printf("myargv[%d]: %s\
", i, myargv[i]);
        }
#endif

}

Makefile file modification (add a macro, in order to achieve conditional compilation)


Through testing, we found that the string segmentation is completed. However, a new problem arises again, does our command line only run once? Of course not, so we need to add an infinite loop to ensure that the command line can be entered multiple times.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //array of pointers

int main()
{<!-- -->
    while(1)
    {<!-- -->
        // output prompt
        printf("username@hostname current path#");
        fflush(stdout);

        // Get user input, when inputting, enter\

        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // Clear the last \
 , abcd\

        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\
", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // String cutting
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;

        // If there is no substring, strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));


        // test success, conditional compilation
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i ++ )
        {<!-- -->
            printf("myargv[%d]: %s\
", i, myargv[i]);
        }
#endif

    }
}

So far, we have supported multiple input of the command line.

5. Execute command

We choose execvp to get the data, because it has v, p, no need to enter the specific path, only the target file + execution option (array).

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //array of pointers

int main()
{<!-- -->
    while(1)
    {<!-- -->
        // output prompt
        printf("username@hostname current path#");
        fflush(stdout);

        // Get user input, when inputting, enter\

        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // Clear the last \
 , abcd\

        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\
", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // String cutting
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL & amp; & amp; strcmp(myargv[0], "ls") == 0)
        {<!-- -->
            myargv[i++] = (char*)"--color=auto";
        }

        // If there is no substring, strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));


        // test success, conditional compilation
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i ++ )
        {<!-- -->
            printf("myargv[%d]: %s\
", i, myargv[i]);
        }
#endif
        // Excuting an order
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {<!-- -->
            execvp(myargv[0], myargv);//object file + execution option (array)
            exit(1);//Continue to execute later, indicating that the replacement failed, return directly
        }
        int status = 0;
        pid_t ret = waitpid(id, & amp;status, 0); //process waiting

    }
}

So far, we have completed the simplest shell. Of course, there are still some details that need to be added later to make our shell more perfect.

6. ls add color

The input command cannot be empty; under the ls command, add a color to identify the directory or file:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //array of pointers

int main()
{<!-- -->
    while(1)
    {<!-- -->
        // output prompt
        printf("username@hostname current path#");
        fflush(stdout);

        // Get user input, when inputting, enter\

        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // Clear the last \
 , abcd\

        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\
", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // String cutting
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL & amp; & amp; strcmp(myargv[0], "ls") == 0)//The input command cannot be empty; under the ls command, add a color to identify the directory or file
        {<!-- -->
            myargv[i + + ] = (char*)"--color=auto"; // add color here! ! !
        }

        // If there is no substring, strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));


        // test success, conditional compilation
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i ++ )
        {<!-- -->
            printf("myargv[%d]: %s\
", i, myargv[i]);
        }
#endif
        // Excuting an order
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {<!-- -->
            execvp(myargv[0], myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
    }
}

7. Implement cd

Through experiments, we found that the path remains unchanged after inputting cd.. in the simple shell we made ourselves. Next, we will implement cd. When implementing path conversion, we need to know what the current path is. In fact, the current path is the working directory of the process.

ls /proc/process pid -al #Command to view process attributes

Why can’t our own shell cd? Because we change the directory in the child process after fork creates the child process. The child process has its own working directory. The change of the child process directory does not affect the working directory of the parent process. After the child process ends, we still use is the parent process, the shell. In other words, after we cd, and then enter the pwd command, the path will not change, because pwd is executed by another child process created by the parent process!

The current working directory can be modified, we can use chdir to change the current directory.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //array of pointers

int main()
{<!-- -->
    while(1)
    {<!-- -->
        // output prompt
        printf("username@hostname current path#");
        fflush(stdout);

        // Get user input, when inputting, enter\

        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // Clear the last \
 , abcd\

        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\
", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // String cutting
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL & amp; & amp; strcmp(myargv[0], "ls") == 0)
        {<!-- -->
            myargv[i++] = (char*)"--color=auto";
        }

        // If there is no substring, strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

        // If it is a cd command, there is no need to create a subprocess, let the shell execute the corresponding command itself, the essence is to execute the system interface
        // Commands like this don't need to be executed by our subprocesses, but let the shell itself execute --- built-in/built-in commands
        if(myargv[0] != NULL & amp; & amp; strcmp(myargv[0], "cd") == 0)
        {<!-- -->
            if(myargv[1] != NULL) chdir(myargv[1]);// chdir(path that needs to be changed)
            continue;
        }

        // test success, conditional compilation
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i ++ )
        {<!-- -->
            printf("myargv[%d]: %s\
", i, myargv[i]);
        }
#endif
        // builtin command --> echo

        // Excuting an order
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {<!-- -->
            execvp(myargv[0], myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);

    }
}

8. Implement echo

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //array of pointers
int lastCode = 0; //A variable is added here for easy output
int lastSig = 0;

int main()
{<!-- -->
    while(1)
    {<!-- -->
        // output prompt
        printf("username@hostname current path#");
        fflush(stdout);

        // Get user input, when inputting, enter\

        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // Clear the last \
 , abcd\

        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\
", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // String cutting
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL & amp; & amp; strcmp(myargv[0], "ls") == 0)
        {<!-- -->
            myargv[i++] = (char*)"--color=auto";
        }

        // If there is no substring, strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

        // If it is a cd command, there is no need to create a subprocess, let the shell execute the corresponding command itself, the essence is to execute the system interface
        // Commands like this don't need to be executed by our subprocesses, but let the shell itself execute --- built-in/built-in commands
        if(myargv[0] != NULL & amp; & amp; strcmp(myargv[0], "cd") == 0)
        {<!-- -->
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        
        // implement echo - here! ! !
        if(myargv[0] != NULL & amp; & amp; myargv[1] != NULL & amp; & amp; strcmp(myargv[0], "echo") == 0)
        {<!-- -->
            if(strcmp(myargv[1], "$?") == 0)
            {<!-- -->
                printf("%d, %d\
", lastCode, lastSig);
            }
            else
            {<!-- -->
                printf("%s\
", myargv[1]);
            }
            continue;
        }
        // test success, conditional compilation
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i ++ )
        {<!-- -->
            printf("myargv[%d]: %s\
", i, myargv[i]);
        }
#endif
        // builtin command --> echo

        // Excuting an order
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {<!-- -->
            execvp(myargv[0], myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status>>8) & amp; 0xFF);//transmit status into digital signal
        lastSig = (status & 0x7F);
    }
}

Conclusion

This is probably the end of the implementation knowledge of the simple command line interpreter. The blogger will continue to update more C ++ related knowledge in the future. It is full of dry goods. If you think the blogger wrote it well, I hope Dear friends, don’t be stingy with the triple in hand! Your support is the motivation for bloggers to persist in creation!