用C编写命令行shell;第一次尝试使用 ncurses/C

发布于 2024-10-30 16:43:39 字数 1445 浏览 6 评论 0原文

我正在开发一个类项目,其中我必须编写一个具有以下要求的命令行 shell:

  • shell 必须能够读取缓冲的输入
  • 缓冲区应为 64 个字符
  • 应处理错误条件
    • 超出缓冲区大小
    • 中断(当信号到达时) - 请参阅 read() 的手册页
    • 无效输入(无法解析的字符、空行等)
    • 可能遇到的任何其他错误。
  • Shell 必须具有至少 20 项的历史记录,并且历史记录的大小不能是静态的。当历史缓冲区已满时,将删除最旧的项目并添加最新的项目。
  • 程序应该能够在前台或后台运行。 (使用 &)
  • Ctrl-D 将退出 shell
  • Ctrl-C 将打印完整的历史记录
  • 命令“history”也将打印完整的历史记录。最新的项目将位于列表的底部。
  • 所有其他信号将被捕获并在 shell 中显示给用户
  • 程序将使用 read() 命令读取输入,除非支持箭头键

我选择实现历史循环的箭头键,所以我使用 ncurses用于输入,而不是 read()。我认为我使用 strtok() 解析输入,使用 fork() 和 execvp() 运行进程做得很好,但我没有正确实现 ncurses。到目前为止,我所做的就是初始化一个新屏幕,显示提示,然后在按下任何键时出现段错误。不好。

我想问题一定出在我的设计上。我不太了解 ncurses。我应该在这个项目中使用什么类型的数据结构?我应该如何处理 ncurses 设置、拆卸以及其间的所有事情?窗口和屏幕有什么关系?我应该使用一个可全局访问的窗口/屏幕吗?另外,我一直在尝试使用 char* 作为输入缓冲区,使用 char** 作为命令历史记录,但我没有 C 经验,所以尽管阅读了 malloc、calloc 和 realloc,但我还是不确定在缓冲区和历史记录中存储命令的最佳方式。关于管理这些字符数组有什么建议吗?

tl;dr:如何正确使用 ncurses 来制作命令行 shell,以及如何使用 C 处理命令内存管理?

我意识到这是一个相当沉重的问题。 :(

编辑:我已经看到 http: //www.gnu.org/software/libc/manual/html_node/Implementing-a-Shell.htmlhttp://www.linuxinfor.com/english/NCURSES-Programming/ 但 ncurses 文档实际上有太多开销,我只是想使用它识别箭头键的功能。

I'm working on a class project in which I must write a command line shell with the following requirements:

  • The shell must able to read buffered input
  • Buffer should be 64 characters
  • Error conditions should be handled
    • Exceeded buffer size
    • Interruptions (when a signal arrives) – see the man page for read()
    • Invalid input (unparsable characters, blank lines, etc)
    • Any other error that may be encountered.
  • Shell must have a history of at least 20 items, and the history must not be of a static size. When the history buffer is full, the oldest item is removed and the newest item added.
  • Programs should be able to run in the foreground or background. (using &)
  • Ctrl-D will exit the shell
  • Ctrl-C will print the complete history
  • The Command ‘history’ will also print the complete history. Newest items will be at the bottom of the list.
  • All other signals will be trapped and displayed to the user in the shell
  • Program will use the read() command to read in input, unless the arrow keys are supported

I have opted to implement arrow keys for history cycling, so I'm using ncurses for input, rather than read(). I think I'm doing all right using strtok() to parse input, and fork() and execvp() to run the processes, but I'm not doing all right implementing ncurses correctly. All I've gotten it to do so far is init a new screen, display the prompt, then segfault upon any key press. Not good.

I reckon the problem must be in my design. I'm not wrapping my head around ncurses too well. What sort of data structures should I be using for this project? How should I handle the ncurses setup, teardown, and everything in between? What's the deal with windows and screens, and should I have a single globally accessible window/screen that I work with? Also, I've been trying to use a char* for the input buffer, and a char** for the command history, but I have no experience in C, so despite reading up on malloc, calloc, and realloc, I'm not sure of the best way to store commands in the buffer and the history. Any tips on managing these char arrays?

tl;dr: How do I use ncurses correctly to make a command line shell, and how do I handle the command memory management with C?

I realize this is a pretty hefty question. :(

edit: I have already seen http://www.gnu.org/software/libc/manual/html_node/Implementing-a-Shell.html and http://www.linuxinfor.com/english/NCURSES-Programming/ but the ncurses documentation has actually too much overhead. I just want to use its ability to recognize arrow keys.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

我三岁 2024-11-06 16:43:39

下面是一些示例代码:

  1. 执行动态内存分配。

  2. 以非阻塞模式从控制台读取。

  3. 使用 VT100 代码将帧缓冲区打印到控制台。

它使用 GCC 在 Linux 上编译,没有警告或错误。它远非没有错误,但它应该给您一些关于可能性的想法。编译并运行它,按[向上]和[向下]将打印消息,输入字符并按[输入]将“执行”命令。

#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/** VT100 command to clear the screen. Use puts(VT100_CLEAR_SCREEN) to clear
 *  the screen. */
#define VT100_CLEAR_SCREEN "\033[2J"

/** VT100 command to reset the cursor to the top left hand corner of the
 *  screen. */
#define VT100_CURSOR_TO_ORIGIN "\033[H"

struct frame_s
{
    int x;
    int y;
    char *data;
};

static int draw_frame(struct frame_s *frame)
{
    int row;
    char *data;
    int attrib;

    puts(VT100_CLEAR_SCREEN);
    puts(VT100_CURSOR_TO_ORIGIN);

    for (row = 0, data = frame->data; row  < frame->y; row++, data += frame->x)
    {
        /*  0 for normal, 1 for bold, 7 for reverse. */
        attrib = 0;

        /*  The VT100 commands to move the cursor, set the attribute, and the
         *  actual frame line. */
        fprintf(stdout, "\033[%d;%dH\033[0m\033[%dm%.*s", row + 1, 0, attrib, frame->x, data);
        fflush(stdout);
    }

    return (0);
}

int main(void)
{
    const struct timespec timeout = { .tv_sec = 1, .tv_nsec = 0 };
    struct frame_s frame;
    struct termios tty_old;
    struct termios tty_new;
    unsigned char line[128];
    unsigned int count = 0;
    int ret;
    struct pollfd fds[1];
    sigset_t sigmask;
    struct tm *tp;
    time_t current_time;

    /*  Set up a little frame. */
    frame.x = 80;
    frame.y = 5;
    frame.data = malloc(frame.x * frame.y);

    if (frame.data == NULL)
    {
        fprintf(stderr, "No memory\n");
        exit (1);
    }

    memset(frame.data, ' ', frame.x * frame.y);

    /*  Get the terminal state. */
    tcgetattr(STDIN_FILENO, &tty_old);
    tty_new = tty_old;

    /*  Turn off "cooked" mode (line buffering) and set minimum characters
     *  to zero (i.e. non-blocking). */
    tty_new.c_lflag &= ~ICANON;
    tty_new.c_cc[VMIN] = 0;

    /*  Set the terminal attributes. */
    tcsetattr(STDIN_FILENO, TCSANOW, &tty_new);

    /*  Un-mask all signals while in ppoll() so any signal will cause
     *  ppoll() to return prematurely. */
    sigemptyset(&sigmask);

    fds[0].events = POLLIN;
    fds[0].fd = STDIN_FILENO;

    /*  Loop forever waiting for key presses. Update the output on every key
     *  press and every 1.0s (when ppoll() times out). */
    do
    {
        fds[0].revents = 0;
        ret = ppoll(fds, sizeof(fds) / sizeof(struct pollfd), &timeout, &sigmask);

        if (fds[0].revents & POLLIN)
        {
            ret = read(STDIN_FILENO, &line[count], sizeof(line) - count);

            if (ret > 0)
            {
                line[count + ret] = '\0';

                if (strcmp(&line[count], "\033[A") == 0)
                {
                    snprintf(frame.data, frame.x, "up");
                    count = 0;
                }
                else if (strcmp(&line[count], "\033[B") == 0)
                {
                    snprintf(frame.data, frame.x, "down");
                    count = 0;
                }
                else if (line[count] == 127) // backspace
                {
                    if (count != 0) { count -= ret;}
                }
                else if (line[count] == '\n')
                {
                    snprintf(frame.data, frame.x, "entered: %s", line);
                    count = 0;
                }
                else
                {
                    count += ret;
                }
            }
        }

        /*  Print the current time to the output buffer. */
        current_time = time(NULL);
        tp = localtime(¤t_time);
        strftime(&frame.data[1 * frame.x], frame.x, "%Y/%m/%d %H:%M:%S", tp);

        /*  Print the command line. */
        line[count] = '\0';
        snprintf(&frame.data[(frame.y - 1) * frame.x], frame.x, "$ %s", line);

        draw_frame(&frame);
    }
    while (1);

    /*  Restore terminal and free resources. */
    tcsetattr(STDIN_FILENO, TCSANOW, &tty_old);
    free(frame.data);

    return (0);
}

Here's some sample code which:

  1. Performs dynamic memory allocation.

  2. Reads from the console in non-blocking mode.

  3. Uses VT100 codes to print a frame buffer to the console.

It compiles on Linux using GCC without warnings or errors. It's far from bug free, but it should give you some ideas of what's possible. Compile and run it, pressing [up] and [down] will print messages, typing characters and hitting [enter] will "execute" the command.

#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/** VT100 command to clear the screen. Use puts(VT100_CLEAR_SCREEN) to clear
 *  the screen. */
#define VT100_CLEAR_SCREEN "\033[2J"

/** VT100 command to reset the cursor to the top left hand corner of the
 *  screen. */
#define VT100_CURSOR_TO_ORIGIN "\033[H"

struct frame_s
{
    int x;
    int y;
    char *data;
};

static int draw_frame(struct frame_s *frame)
{
    int row;
    char *data;
    int attrib;

    puts(VT100_CLEAR_SCREEN);
    puts(VT100_CURSOR_TO_ORIGIN);

    for (row = 0, data = frame->data; row  < frame->y; row++, data += frame->x)
    {
        /*  0 for normal, 1 for bold, 7 for reverse. */
        attrib = 0;

        /*  The VT100 commands to move the cursor, set the attribute, and the
         *  actual frame line. */
        fprintf(stdout, "\033[%d;%dH\033[0m\033[%dm%.*s", row + 1, 0, attrib, frame->x, data);
        fflush(stdout);
    }

    return (0);
}

int main(void)
{
    const struct timespec timeout = { .tv_sec = 1, .tv_nsec = 0 };
    struct frame_s frame;
    struct termios tty_old;
    struct termios tty_new;
    unsigned char line[128];
    unsigned int count = 0;
    int ret;
    struct pollfd fds[1];
    sigset_t sigmask;
    struct tm *tp;
    time_t current_time;

    /*  Set up a little frame. */
    frame.x = 80;
    frame.y = 5;
    frame.data = malloc(frame.x * frame.y);

    if (frame.data == NULL)
    {
        fprintf(stderr, "No memory\n");
        exit (1);
    }

    memset(frame.data, ' ', frame.x * frame.y);

    /*  Get the terminal state. */
    tcgetattr(STDIN_FILENO, &tty_old);
    tty_new = tty_old;

    /*  Turn off "cooked" mode (line buffering) and set minimum characters
     *  to zero (i.e. non-blocking). */
    tty_new.c_lflag &= ~ICANON;
    tty_new.c_cc[VMIN] = 0;

    /*  Set the terminal attributes. */
    tcsetattr(STDIN_FILENO, TCSANOW, &tty_new);

    /*  Un-mask all signals while in ppoll() so any signal will cause
     *  ppoll() to return prematurely. */
    sigemptyset(&sigmask);

    fds[0].events = POLLIN;
    fds[0].fd = STDIN_FILENO;

    /*  Loop forever waiting for key presses. Update the output on every key
     *  press and every 1.0s (when ppoll() times out). */
    do
    {
        fds[0].revents = 0;
        ret = ppoll(fds, sizeof(fds) / sizeof(struct pollfd), &timeout, &sigmask);

        if (fds[0].revents & POLLIN)
        {
            ret = read(STDIN_FILENO, &line[count], sizeof(line) - count);

            if (ret > 0)
            {
                line[count + ret] = '\0';

                if (strcmp(&line[count], "\033[A") == 0)
                {
                    snprintf(frame.data, frame.x, "up");
                    count = 0;
                }
                else if (strcmp(&line[count], "\033[B") == 0)
                {
                    snprintf(frame.data, frame.x, "down");
                    count = 0;
                }
                else if (line[count] == 127) // backspace
                {
                    if (count != 0) { count -= ret;}
                }
                else if (line[count] == '\n')
                {
                    snprintf(frame.data, frame.x, "entered: %s", line);
                    count = 0;
                }
                else
                {
                    count += ret;
                }
            }
        }

        /*  Print the current time to the output buffer. */
        current_time = time(NULL);
        tp = localtime(¤t_time);
        strftime(&frame.data[1 * frame.x], frame.x, "%Y/%m/%d %H:%M:%S", tp);

        /*  Print the command line. */
        line[count] = '\0';
        snprintf(&frame.data[(frame.y - 1) * frame.x], frame.x, "$ %s", line);

        draw_frame(&frame);
    }
    while (1);

    /*  Restore terminal and free resources. */
    tcsetattr(STDIN_FILENO, TCSANOW, &tty_old);
    free(frame.data);

    return (0);
}
欢你一世 2024-11-06 16:43:39

如果您的输入缓冲区定义为 64 个字符,那么我建议使用 char 数组而不是 char*。像 char input_buffer[65]; 这样的东西应该可以满足您的目的(为尾随的 '\0' 添加一个额外的字符)。

就命令历史记录而言,您可以使用二维数组。像 char command_history[20][65]; 这样的东西应该可以让您存储 20 个旧命令,每个命令有 64 个字符。

静态分配这些缓冲区应该会让事情变得更容易,因为您不必担心 malloc 和朋友。

如果没有看到你的代码,很难给你太多具体的建议。我有一种感觉,您正在犯与初次学习 C 的人所犯的常见错误相同类型的错误。您能否发布给您带来问题的代码部分,以便我们可以更多地了解您在做什么?

发布提供的代码后进行更新:

我看到的一个问题是函数 takeInput 没有 return 语句。当您在主函数中使用 input = takeInput(); 时,input 的值并未设置为您认为的值。它可能不是一个有效的指针,这会导致您的 input[j] 行出现段错误。

您对 cmdHistory 的使用也需要重新审视。您可以使用 cmdHistory = (char**)calloc(21,sizeof(int)); 来分配它,这样您就有足够的空间来存储 21 个整数。在函数 printHistory 中,您将 cmdHistory 的元素传递给 printw,就好像它们是字符串一样(它们只是整数)。这绝对不是在做你想做的事。相反,cmdHistory 的分配逻辑需要看起来更像您的解除分配逻辑(向后除外)。分配一个 char** 数组,然后迭代该数组,将每个指针分配给新分配的缓冲区。就像数组中的每个元素都有一个 free 语句,加上整个数组有一个 free 语句一样,您也应该有一个 malloc每个元素加上一个 malloc 作为整个数组。

即使您无法使用静态分配的堆栈,也请尝试使用静态分配的堆栈来编写程序。这将使您解决关键检测逻辑等问题,而不必担心程序的动态内存部分。一旦其余部分正常工作,返回并交换静态内存以进行动态内存分配。这样,您一次只需调试一点点。

If your input buffer is defined to be 64 characters, then I would recommend using a char array instead of a char*. Something like char input_buffer[65]; should serve your purposes (add an extra character for the trailing '\0').

As far as command history goes, you can use a two-dimensional array for that. Something like char command_history[20][65]; should let you store 20 old commands of 64 characters each.

Allocating these buffers statically should make things a bit easier for you, as you won't have to worry about malloc and friends.

It's hard to give you too much specific advice without seeing your code. I have a feeling that you are making the same type of mistakes that are typical to people first learning C. Can you post the part of your code that is giving you problems so that we can learn more about what you are doing?

Update after posted provided code:

One problem I'm seeing is that the function takeInput doesn't have a return statement. When you use input = takeInput(); inside your main function, the value of input isn't being set to what you think it is. It's probably not a valid pointer, which is causing your line that says input[j] to segfault.

Your usage of cmdHistory also needs revisiting. You allocate it with cmdHistory = (char**)calloc(21,sizeof(int));, which gives you enough space to store 21 integers. In the function printHistory, you pass elements of cmdHistory to printw as if they were strings (they're only integers). This is most definitely not doing what you want it to do. Instead, your allocation logic for cmdHistory needs to look more like your de-allocation logic (except backwards). Allocate an array of char**, then iterate through the array, assigning each pointer to a newly-allocated buffer. Just like you have one free statement for each element in the array plus a free for the array as a whole, you should have one malloc for each element plus one malloc for the array as a whole.

Even if you can't use a statically-allocated stack, try writing your program using one anyway. This will let you work the kinks out of your key detection logic, etc without having to worry about the dynamic memory part of the program. Once the rest of it is working, go back in and swap out the static memory for dynamic memory allocation. That way, you're only having to debug a little bit at a time.

夜雨飘雪 2024-11-06 16:43:39

你看过 Readline 图书馆吗?它非常适合在您的项目中使用。

http://cnswww.cns.cwru.edu/php/chet/readline /rltop.html

Have you looked at the Readline library? It's ideal for use in your project.

http://cnswww.cns.cwru.edu/php/chet/readline/rltop.html

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文