Giter VIP home page Giter VIP logo

letter-shell's Introduction

letter shell 3.x

version standard build license

一个功能强大的嵌入式shell

shell_info.png

简介

letter shell是一个C语言编写的,可以嵌入在程序中的嵌入式shell,主要面向嵌入式设备,以C语言函数为运行单位,可以通过命令行调用,运行程序中的函数

相对2.x版本,letter shell 3.x增加了用户管理,权限管理,以及对文件系统的初步支持

此外3.x版本修改了命令格式和定义,2.x版本的工程需要经过简单的修改才能完成迁移

若只需要使用基础功能,可以使用letter shell 2.x版本

使用说明可参考Letter shell 3.0 全新出发

如果从3.0版本迁移到3.1以上版本,请注意3.1版本对读写函数原型的修改

功能

  • 命令自动补全
  • 快捷键功能定义
  • 命令权限管理
  • 用户管理
  • 变量支持
  • 代理函数和参数代理解析

移植说明

  1. 定义shell对象

    Shell shell;
  2. 定义shell读,写函数

    对于使用letter shell 3.0版本,读写函数原型如下:

    /**
     * @brief shell读取数据函数原型
     *
     * @param char shell读取的字符
     *
     * @return char 0 读取数据成功
     * @return char -1 读取数据失败
     */
    typedef signed char (*shellRead)(char *);
    
    /**
     * @brief shell写数据函数原型
     *
     * @param const char 需写的字符
     */
    typedef void (*shellWrite)(const char);

    对于使用letter shell 3.1版本,为了优化效率,修改了读写函数原型,如下:

    /**
     * @brief shell读取数据函数原型
     *
     * @param data shell读取的字符
     * @param len 请求读取的字符数量
     *
     * @return unsigned short 实际读取到的字符数量
     */
    typedef unsigned short (*shellRead)(char *data, unsigned short len);
    
    /**
     * @brief shell写数据函数原型
     *
     * @param data 需写的字符数据
     * @param len 需要写入的字符数
     *
     * @return unsigned short 实际写入的字符数量
     */
    typedef unsigned short (*shellWrite)(char *data, unsigned short len);
  3. 申请一片缓冲区

    char shellBuffer[512];
  4. 调用shellInit进行初始化

    shell.read = shellRead;
    shell.write = shellWrite;
    shellInit(&shell, shellBuffer, 512);
  5. 调用(建立)shell任务

    对于运行在操作系统的情况,建立shellTask任务(确保sell_cfg.h中的配置无误),任务参数为shell对象

    OsTaskCreate(shellTask, &shell, ...);

    对于裸机环境,在主循环中调用shellTask,或者在接收到数据时,调用shellHandler

  6. 说明

    • 对于中断方式使用shell,不用定义shell->read,但需要在中断中调用shellHandler
    • 对于使用操作系统的情况,使能SHEHLL_TASK_WHILE宏,然后创建shellTask任务
  7. 其他配置

    • 定义宏SHELL_GET_TICK()为获取系统tick函数,使能tab双击操作,用户长帮助补全
  8. 配置宏

    shell_cfg.h文件中包含了所有用于配置shell的宏,在使用前,需要根据需要进行配置

    建议采用 overlay 的方式,新建一个头文件,例如 shell_cfg_user.h,然后定义编译宏 SHELL_CFG_USER="shell_cfg_user.h",在这个头文件中添加需要修改的配置即可

    意义
    SHELL_TASK_WHILE 是否使用默认shell任务while循环
    SHELL_USING_CMD_EXPORT 是否使用命令导出方式
    SHELL_USING_COMPANION 是否使用shell伴生对象功能
    SHELL_SUPPORT_END_LINE 是否支持shell尾行模式
    SHELL_HELP_LIST_USER 是否在输入命令列表中列出用户
    SHELL_HELP_LIST_VAR 是否在输入命令列表中列出变量
    SHELL_HELP_LIST_KEY 是否在输入命令列表中列出按键
    SHELL_ENTER_LF 使用LF作为命令行回车触发
    SHELL_ENTER_CR 使用CR作为命令行回车触发
    SHELL_ENTER_CRLF 使用CRLF作为命令行回车触发
    SHELL_EXEC_UNDEF_FUNC 使用执行未导出函数的功能
    SHELL_COMMAND_MAX_LENGTH shell命令最大长度
    SHELL_PARAMETER_MAX_NUMBER shell命令参数最大数量
    SHELL_HISTORY_MAX_NUMBER 历史命令记录数量
    SHELL_DOUBLE_CLICK_TIME 双击间隔(ms)
    SHELL_QUICK_HELP 快速帮助
    SHELL_MAX_NUMBER 管理的最大shell数量
    SHELL_GET_TICK() 获取系统时间(ms)
    SHELL_USING_LOCK 是否使用锁
    SHELL_MALLOC(size) 内存分配函数(shell本身不需要)
    SHELL_FREE(obj) 内存释放函数(shell本身不需要)
    SHELL_SHOW_INFO 是否显示shell信息
    SHELL_CLS_WHEN_LOGIN 是否在登录后清除命令行
    SHELL_DEFAULT_USER shell默认用户
    SHELL_DEFAULT_USER_PASSWORD 默认用户密码
    SHELL_LOCK_TIMEOUT shell自动锁定超时
    SHELL_USING_FUNC_SIGNATURE 使用函数签名
    SHELL_SUPPORT_ARRAY_PARAM 支持数组参数

使用方式

函数定义

letter shell 3.x同时支持两种形式的函数定义方式,形如main函数定义的func(int argc, char *agrv[])以及形如普通C函数的定义func(int i, char *str, ...),两种函数定义方式适用于不同的场景

main函数形式

使用此方式,一个函数定义的例子如下:

int func(int argc, char *argv[])
{
    printf("%dparameter(s)\r\n", argc);
    for (char i = 1; i < argc; i++)
    {
        printf("%s\r\n", argv[i]);
    }
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN), func, func, test);

终端调用

letter:/$ func "hello world"
2 parameter(s)
hello world

普通C函数形式

使用此方式,shell会自动对参数进行转化处理,目前支持二进制,八进制,十进制,十六进制整形,字符,字符串的自动处理,如果需要其他类型的参数,请使用代理参数解析的方式(参考代理函数和代理参数解析),或者使用字符串的方式作为参数,自行进行处理,例子如下:

int func(int i, char ch, char *str)
{
    printf("input int: %d, char: %c, string: %s\r\n", i, ch, str);
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), func, func, test);

终端调用

letter:/$ func 666 'A' "hello world"
input int: 666, char: A, string: hello world

变量使用

letter shell 3.x支持导出变量,通过命令行查看,设置以及使用变量的值

  • 导出变量

    变量导出使用SHELL_EXPORT_VAR宏,支持整形(char, short, int),字符串,指针以及节点变量,变量导出需要使用引用的方式,如果不允许对变量进行修改,在属性中添加SHELL_CMD_READ_ONLY

    int varInt = 0;
    SHELL_EXPORT_VAR(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_VAR_INT), varInt, &varInt, test);
    
    char str[] = "test string";
    SHELL_EXPORT_VAR(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_VAR_STRING), varStr, str, test);
    
    Log log;
    SHELL_EXPORT_VAR(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_VAR_POINT), log, &log, test);
  • 查看变量

    在命令行直接输入导出的变量名即可查看变量当前的值

    letter:/$ varInt
    varInt = 0, 0x00000000
    
    letter:/$ varStr
    varStr = "test string"
  • 修改变量

    使用setVar命令修改变量的值,对于字符串型变量,请确认字符串有分配足够的空间,指针类型的变量不可修改

    letter:/$ setVar varInt 45678
    varInt = 45678, 0x0000b26e
    
    letter:/$ setVar varStr "hello"
    varStr = "hello"
  • 使用变量

    letter shell 3.x的变量可以在命令中作为参数传递,对于需要传递结构体引用到命令中的场景特别适用,使用$+变量名的方式传递

    letter:/$ shellPrint $shell "hello world\r\n"
    hello world

在函数中获取当前shell对象

letter shell采取一个静态数组对定义的多个shell进行管理,shell数量可以修改宏SHELL_MAX_NUMBER定义(为了不使用动态内存分配,此处通过数据进行管理),从而,在shell执行的函数中,可以调用shellGetCurrent()获得当前活动的shell对象,从而可以实现某一个函数在不同的shell对象中发生不同的行为,也可以通过这种方式获得shell对象后,调用shellWriteString(shell, string)进行shell的输出

执行未导出函数

letter shell支持通过函数地址直接执行函数,可以方便执行那些没有导出,但是又临时需要使用的函数,使用命令exec [addr] [args]执行,使用此功能需要开启SHELL_EXEC_UNDEF_FUNC宏,注意,由于直接操作函数地址执行,如果给进的地址有误,可能引起程序崩溃

函数的地址可以通过编译生成的文件查找,比如说对于keil,可以在.map文件中查找到每个函数的地址,但是要注意有些平台可能需要要对地址做进一步处理,比如说对于 arm 平台,如果使用的是 Thumb 指令集,那么需要将地址的最低位置 1,比如说shellClear函数地址为0x08028620,则通过exec执行应为exec 0x08028621

其他编译器查找函数地址的方式和地址偏移的处理,请参考各编译器手册

命令定义

letter shell 3.x将可执行的函数命令定义,用户定义,按键定义以及变量定义统一归为命令定义,使用相同的结构储存,查找和执行

定义方式

letter shell 支持使用命令导出方式和命令表方式进行命令的添加,定义,通过宏SHELL_USING_CMD_EXPORT控制

命令导出方式支持keil,IAR以及GCC

  1. 命令导出方式

    letter shell 支持在函数体外部,采用定义常量的方式定义命令,例如SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE (SHELL_TYPE_CMD_MAIN)|SHELL_CMD_DISABLE_RETURN,help, shellHelp, show command info\r\nhelp [cmd]);

    对于使用keil进行编译,需要在keil的target option中增加--keep shellCommand*,防止定义的命令被优化掉

    使用GCC编译时,需要在ld文件中的只读数据区(建议)添加:

    _shell_command_start = .;
    KEEP (*(shellCommand))
    _shell_command_end = .;
    
  2. 命令表方式

    • 当使用其他暂时不支持使用命令导出方式的编译器时,需要在shell_cmd_list.c文件的命令表中添加

      const SHELL_CommandTypeDef shellDefaultCommandList[] =
      {
          SHELL_CMD_ITEM(
                     SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN)|SHELL_CMD_DISABLE_RETURN,
                     help, shellHelp, show command info\r\nhelp [cmd]),
      };

定义宏说明

letter shell 3.x对可执行命令,按键,用户以及变量分别提供了一个宏,用于进行命令定义

  1. 可执行命令定义

    使用宏SHELL_EXPORT_CMD定义可执行命令,定义如下

    /**
     * @brief shell 命令定义
     *
     * @param _attr 命令属性
     * @param _name 命令名
     * @param _func 命令函数
     * @param _desc 命令描述
     */
    #define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \
            const char shellCmd##_name[] = #_name; \
            const char shellDesc##_name[] = #_desc; \
            SHELL_USED const ShellCommand \
            shellCommand##_name SHELL_SECTION("shellCommand") =  \
            { \
                .attr.value = _attr, \
                .data.cmd.name = shellCmd##_name, \
                .data.cmd.function = (int (*)())_func, \
                .data.cmd.desc = shellDesc##_name \
            }
  2. 变量定义

    使用宏SHELL_EXPORT_VAR定义变量,定义如下

    /**
     * @brief shell 变量定义
     *
     * @param _attr 变量属性
     * @param _name 变量名
     * @param _value 变量值
     * @param _desc 变量描述
     */
    #define SHELL_EXPORT_VAR(_attr, _name, _value, _desc) \
            const char shellCmd##_name[] = #_name; \
            const char shellDesc##_name[] = #_desc; \
            SHELL_USED const ShellCommand \
            shellVar##_name SHELL_SECTION("shellCommand") =  \
            { \
                .attr.value = _attr, \
                .data.var.name = shellCmd##_name, \
                .data.var.value = (void *)_value, \
                .data.var.desc = shellDesc##_name \
            }

    变量定义时,_value应该是变量的引用,如果变量不允许修改,则需要在增加SHELL_CMD_READ_ONLY属性

  3. 用户定义

    使用宏SHELL_EXPORT_USER定义用户,定义如下

    /**
     * @brief shell 用户定义
     *
     * @param _attr 用户属性
     * @param _name 用户名
     * @param _password 用户密码
     * @param _desc 用户描述
     */
    #define SHELL_EXPORT_USER(_attr, _name, _password, _desc) \
            const char shellCmd##_name[] = #_name; \
            const char shellPassword##_name[] = #_password; \
            const char shellDesc##_name[] = #_desc; \
            SHELL_USED const ShellCommand \
            shellUser##_name SHELL_SECTION("shellCommand") =  \
            { \
                .attr.value = _attr|SHELL_CMD_TYPE(SHELL_TYPE_USER), \
                .data.user.name = shellCmd##_name, \
                .data.user.password = shellPassword##_name, \
                .data.user.desc = shellDesc##_name \
            }
  4. 按键定义

    使用宏SHELL_EXPORT_KEY定义按键,定义如下

    /**
     * @brief shell 按键定义
     *
     * @param _attr 按键属性
     * @param _value 按键键值
     * @param _func 按键函数
     * @param _desc 按键描述
     */
    #define SHELL_EXPORT_KEY(_attr, _value, _func, _desc) \
            const char shellDesc##_value[] = #_desc; \
            SHELL_USED const ShellCommand \
            shellKey##_value SHELL_SECTION("shellCommand") =  \
            { \
                .attr.value = _attr|SHELL_CMD_TYPE(SHELL_TYPE_KEY), \
                .data.key.value = _value, \
                .data.key.function = (void (*)(Shell *))_func, \
                .data.key.desc = shellDesc##_value \
            }

    按键键值为在终端输入按键会发送的字符串序列,以大端模式表示,比如在SecureCRT中断,按下Tab键,会发送0x0B,则这个按键的键值为0x0B000000,如果按下方向上,会依次发送0x1B, 0x5B, 0x41, 则这个键的键值为0x1B5B4100

命令属性字段说明

在命令定义中,有一个attr字段,表示该命令的属性,具体定义为

union
{
    struct
    {
        unsigned char permission : 8;                       /**< command权限 */
        ShellCommandType type : 4;                          /**< command类型 */
        unsigned char enableUnchecked : 1;                  /**< 在未校验密码的情况下可用 */
        unsigned char  readOnly : 1;                        /**< 只读 */
        unsigned char reserve : 1;                          /**< 保留 */
        unsigned char paramNum : 4;                         /**< 参数数量 */
    } attrs;
    int value;
} attr;

在定义命令时,需要给定这些值,可以通过宏SHELL_CMD_PERMISSION(permission), SHELL_CMD_TYPE(type), SHELL_CMD_ENABLE_UNCHECKED, SHELL_CMD_DISABLE_RETURN, SHELL_CMD_READ_ONLY, SHELL_CMD_PARAM_NUM(num)快速声明

代理函数和代理参数解析

letter shell 3.x原生支持将整数,字符,字符串参数,以及在某些情况下的浮点参数直接传递给执行命令的函数,一般情况下,这几种参数类型完全可以满足调试需要,然而在某些情况下,用户确实需要传递其他类型的参数,此时,可以选择将命令定义成main函数形式,使用字符串传递参数,然后自行对参数进行解析,除此之外,letter shell还提供了代理函数的机制,可以对任意类型的参数进行自定义解析

关于代理函数的实现原理和具体使用示例,可以参考letter-shell代理函数解析

使用代理函数,用户需要自定义代理参数解析器,即一个将基本参数(整数,字符,字符串参数)转换成目标类型参数的函数或者宏,letter shell默认实现了浮点类型的参数解析器SHELL_PARAM_FLOAT(x)

然后,使用代理函数命令导出宏定义命令,比如需要需要传递多个浮点参数的函数,如下

void test(int a, float b, int c, float d)
{
    printf("%d, %f, %d, %f \r\n", a, b, c, d);
}
SHELL_EXPORT_CMD_AGENCY(SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),
test, test, test,
p1, SHELL_PARAM_FLOAT(p2), p3, SHELL_PARAM_FLOAT(p4));

相比常规的命令导出,代理函数命令导出前4个参数和常规形式的命令导出一致,之后的参数即传递至目标函数的参数,letter shell默认实现的代理函数定义支持最多7个参数,p1~p7,对于不需要代理参数解析的参数,只需要对应写入px(x为1~7)即可,比如上方示例的p1p3,而需要代理参数解析的参数,则需要使用对应的参数解析器,比如上方示例的p2p4

函数签名

letter shell 3.2.x 之后,引入了函数签名的概念,以便于参数自动解析

之前的版本里,如果声明的命令是 SHELL_TYPE_CMD_FUNC,shell 会自动进行参数的转换,但是参数转换后的类型是猜出来的,无法保证转换后的数据类型是正确的,一旦猜错了,就容易导致程序挂掉

由此,借鉴 Java 等语言的函数签名,新版也引入了函数签名的概念,在声明命令时,可以给定最终执行命令的函数的签名,shell 根据这个签名进行参数转换,使用此功能时,需要打开宏 SHELL_USING_FUNC_SIGNATURE

函数签名是一个字符串,通过这个字符串声明表达函数的参数类型,返回值不声明,比如一个函数int func(int a, char *b, char c),它的函数签名就是 isc

基本类型的参数签名定义如下:

类型 签名
char(字符) c
char(数字) q
short(数字) h
int(数字) i
char * (字符串) s
pointer p

声明命令时,在最后添加一个参数 .data.cmd.signature = "isc" 即可,比如:

void shellFuncSignatureTest(int a, char *b, char c)
{
    printf("a = %d, b = %s, c = %c\r\n", a, b, c);
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),
funcSignatureTest, shellFuncSignatureTest, test function signature, .data.cmd.signature = "isc");

自定义类型解析

由于函数签名的引用,我们就可以使用函数签名描述任何参数,对应的,在参数类型已知的情况下,也可以定义对应的参数解析器进行参数解析,自定义的参数类型签名需要以 L 开头,以 ; 结尾,比如说定义一个 TestStruct 结构体类型为 LTestStruct;,那么接收这个结构体为参数的函数就可以通过这个类型签名定义函数签名,并导出命令

typedef struct {
    int a;
    char *b;
} TestStruct;

void shellParamParserTest(int a, TestStruct *data, char *c)
{
    printf("a = %d, data->a = %d, data->b = %s, c = %s\r\n", a, data->a, data->b, c);
}
SHELL_EXPORT_CMD_SIGN(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),
paramParserTest, shellParamParserTest, test function signature and param parser, iLTestStruct;s);

同时,我们需要对自定义的类型定义解析器,使用 SHELL_EXPORT_PARAM_PARSER

int testStructParser(char *string, void **param)
{
    TestStruct *data = malloc(sizeof(TestStruct));
    data->b = malloc(16);
    if (sscanf(string, "%d %s", &(data->a), data->b) == 2)
    {
        *param = (void *)data;
        return 0;
    }
    return -1;
}

int testStructClener(void *param)
{
    TestStruct *data = (TestStruct *)param;
    free(data->b);
    free(data);
    return 0;
}
SHELL_EXPORT_PARAM_PARSER(0, LTestStruct;, testStructParser, testStructClener);

SHELL_EXPORT_PARAM_PARSER 接收四个参数,第一个参数表示属性,这里一般填 0 皆可,第二个参数就是解析器对应的类型签名,第三个参数是解析器函数,第四个参数是清理函数,清理函数在参数解析失败或者命令执行完毕后会被调用,一般用于清理解析器分配的内存,如果不需要清理函数,填 NULL 即可

解析器函数接收两个参数,第一个参数是输入的字符串,也就是命令行输入的参数,第二个参数是解析后的参数,解析成功后,需要将解析后的参数赋值给第二个参数,解析成功返回 0,解析失败返回 -1

清理函数接收一个参数,就是解析器函数解析得到的结果

数组参数

letter shell 3.2.2 之后,基于函数签名,我们支持了对数组参数的直接解析,使用时,需要打开宏 SHELL_SUPPORT_ARRAY_PARAM, 并且配置好 SHELL_MALLOCSHELL_FREE

数组的参数签名,只需要在常规参数签名前加上 [, 比如,对于 int 类型的数组,他的签名为 [i

命令行调用时,数组参数使用 [] 包裹,每个元素之间用 , 分隔,比如 func [1,2,3,4]

int shellArrayTest(int a, int *b, TestStruct **datas)
{
    int i;
    printf("a = %d, b = %p, datas = %p\r\n", a, b, datas);
    for (i = 0; i < shellGetArrayParamSize(b); i++)
    {
        printf("b[%d] = %d\r\n", i, b[i]);
    }
    for (i = 0; i < shellGetArrayParamSize(datas); i++)
    {
        printf("datas[%d]->a = %d, datas[%d]->b = %s\r\n", i, datas[i]->a, i, datas[i]->b);
    }
    return 0;
}
SHELL_EXPORT_CMD_SIGN(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),
arrayTest, shellArrayTest, test array param parser, i[i[LTestStruct;);

命令执行如下

letter:/workspaces/letter-shell/demo/x86-gcc$ arrayTest 12 [65, 89, 45] ["100 hello", "56 world"]
a = 12, b = 0x1d2db24, datas = 0x1d2db44
b[0] = 65
b[1] = 89
b[2] = 45
datas[0]->a = 100, datas[0]->b = hello
datas[1]->a = 56, datas[1]->b = world
Return: 0, 0x00000000

注意,使用数组参数时,charshortint 不可以共用 i 的签名,需要分别使用 q (quarter), h (half)

权限系统说明

letter shell 3.x的权限管理同用户定义紧密相关,letter shell 3.x使用8个bit位表示命令权限,当用户和命令的权限按位与为真,或者命令权限为0时,表示该用户拥有此命令的权限,可以调用该命令

锁说明

letter shell 3.1增加了shell锁,主要目的是为了防止shell输出和其他输入(比如说日志)对终端的竞争,导致输出混乱的现象,如果使用场景中没有出现终端输出混乱的情况,可以不使用shell锁

注意: 请使用支持嵌套的锁

  1. 使能宏并实现锁

    使能SHELL_USING_LOCK宏,实现shell上锁和解锁函数,函数原型如下:

    /**
     * @brief shell上锁
     *
     * @param struct shell_def shell对象
     *
     * @return 0
     */
    typedef int (*shellLock)(struct shell_def *);
    
    /**
     * @brief shell解锁
     *
     * @param struct shell_def shell对象
     *
     * @return 0
     */
    typedef int (*shellLock)(struct shell_def *);
  2. 使用锁

    在可能产生终端竞争的地方,加上shell锁,比如如果调用shellPrint进行格式化输出

    SHELL_LOCK(shell);
    shellPrint(shell, ...);
    SHELL_UNLOCK(shell);
  3. 注意

    • 不要在shell命令中调用shell锁,除非实现的shell锁为可嵌套的锁

伴生对象

letter shell 3.0.3版本引入了伴生对象的概念,通过宏SHELL_USING_COMPANION开启或者关闭,若使用伴生对象的功能,需要同时将shell_companion.c文件加入到工程中,伴生对象可以用于需要将某个对象同shell关联的场景,比如说,通过快捷键控制shell终端对应的日志打印对象

一般情况下,使用shellCompanionAdd将伴生对象同shell对象进行关联,之后,可以在shell操作中,通过shellCompanionGet获取相应的伴生对象,以达到在不同的shell中,操作不同对象的目的

尾行模式

letter shell 3.0.4版本新增了尾行模式,适用于需要在shell所使用的交互终端同时输入其他信息(比如说日志)时,防止其他信息的输出,导致shell交互体验极差的情况,使用时,使能宏SHELL_SUPPORT_END_LINE,然后对于其他需要使用终端输入信息的地方,调用shellWriteEndLine接口将信息输入,此时,调用shellWriteEndLine进行输入的内容将会插入到命令行上方,终端会一直保持shell命令行位于最后一行

使用letter shell尾行模式结合log日志输出的效果如下:

end line mode

建议终端软件

  • 对于基于串口移植,letter shell建议使用secureCRT软件,letter shell中的相关按键映射都是按照secureCRT进行设计的,使用其他串口软件时,可能需要修改键值

命令遍历工具

letter shell 3.x提供了一个用于遍历工程中命令导出的工具,位于tools/shellTools.py,需要python3环境运行,可以列出工程中,所有使用SHELL_EXPORT_XXX导出的命令名,以及位置,结合VS Code可以直接进行跳转

python shellTools.py project

注意:shellTools会遍历指定目录中所有文件,所以当工程中文件较多时,速度会比较慢,建议只用于遍历用户模块的目录

x86 demo

letter shell 3.x提供了一个x86的demo,可以直接编译运行,其中包含了一条按键键值测试命令,可以测试按键键值,用于快捷键的定义,编译运行方法如下:

cd demo/x86-gcc/
cmake .
make
./LetterShell

letter-shell's People

Contributors

cctv180 avatar charlesjin6 avatar mrxsean avatar nevermindzzt avatar redoccheng avatar xukaihub avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

letter-shell's Issues

无法显示欢迎页,Tab出现乱码

你好,我是stm32爱好者
使用芯片:STM32F103C8T6
编写环境:win10,keil5,已添加防止优化代码
烧录方式:FlyMcu串口
通讯:USART1,波特率无误,PC端测试能接收未移植前的串口数据
问题描述:使用串口助手(多款),单片机复位后无法显示欢迎界面,只显示“_ 0”等无意义数据,且按任何键都有出现乱码的可以能,特别是Tab键。
1.下图是连续按Tab的结果图示
9021B7B3-D1EF-484B-8B02-691FD1

2.连续复位的结果
03159E2F-34B5-490D-AD81-EE7DA1

希望可以帮助我解决这个问题,因为这个工具我很需要,如果能用起来将会对我以后写程序有莫大帮助,感谢!

问题已经自行解决,原因是写入串口函数用错了!打扰了~

多用户登录的问题

设置多用户后,登录是必须先输入默认用户的命令后,才能进一步切换成其他用户,这里面有几个问题:
1.需要知道默认用户的密码才能切换用户,这个地方设计的时候不太合理
2.默认用户的权限默认是最高的,如果用户知道他的密码后,就没有必要做用户切换了
建议:
还是参照linux中的设定,登录时要同时输入用户名和密码

变量相关操作

变量相关的操作,提几个建议:
1.能否显示变量的地址?好像没找到显示变量地址的功能
2.变量是否能增加一个写操作的属性,比如(0-只读,1-可写,其他视为非内存变量,需提供对应写操作函数进行修改),主要是目前没有相关的操作,有些flash中的变量调用修改的操作,会直接跑飞

对于2.x版本兼容的问题

首先非常感谢作者提供的这个开源的shell,一直跟踪使用了1年多,对我的帮助非常大,调试和产品维护方便了许多,后续会一直跟进这作者的升级,谢谢啦 O(∩_∩)O哈哈~

作者这次3.0版本是做了比较大的改动了,对老版本完全不兼容了,移植方面到还好了,整体改动不大,主要是命令导出方式也做了调整,这就导致项目中很多地方要做修改,建议:

1.将新版本的命令导出改成:

SHELL_EXPORT_CMD_EX

2.新增兼容型宏:

    //老版本导出命令
    #define SHELL_EXPORT_CMD(cmd, func, desc)   \
            SHELL_EXPORT_CMD_EX(                \
            SHELL_CMD_PERMISSION(0)|            \
            SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN)|\
            SHELL_CMD_DISABLE_RETURN,           \
            cmd,                                \
            func,                               \
            desc                                \
            )  

上下左右和删除键卡死

移植完成后,Tab键和其它按键都没有问题,但是一旦按下上下左右键之后(不管什么时候),Shell就无响应了,按什么就回什么。调试的时候发现上下左右获取不了键值。我是用的Xshell,查看了键值,是和程序预设值一样的。

多线程打印log出现交叉打印的情况

1、使用的尾行模式
2、由于shell留出的接口是写1个字节,在这里添加互斥锁只能减少出现的概率,

不知道你们有没有遇到这个问题,怎么解决的

配置有密码的情况下,初始化,shellSetUser函数shell->parser.param[1]还未赋值,导致崩溃。

static void shellSetUser(Shell *shell, const ShellCommand *user)
{
shell->info.user = user;
shell->status.isChecked = 1;
((user->data.user.password && strlen(user->data.user.password) != 0)
&& (shell->parser.paramCount == 1
|| strcmp(user->data.user.password, shell->parser.param[1]) != 0))
? 0 : 1;

#if SHELL_CLS_WHEN_LOGIN == 1
shellWriteString(shell, shellText[SHELL_TEXT_CLEAR_CONSOLE]);
#endif
#if SHELL_SHOW_INFO == 1
if (shell->status.isChecked)
{
shellWriteString(shell, shellText[SHELL_TEXT_INFO]);
}
#endif
}

配置有密码的情况下,初始化,该函数shell->parser.param[1]还未赋值,导致崩溃。

建议:代码数据类型关键字使用

建议代码中不要出现具体的数据类型如char、int、short等等,因为这些具体的类型在不同的编译环境中的长度是不同的,尤其是char,有的编译器默认是无符号的、有的默认是有符号的,这会给移植带来问题。

数据类型一律统一定义、见其名知其意,如可定义:
#define u8 unsigned char
#define s8 signed char
... ...
代码中只用s8、或者u8,这样在移植时,只需要根据不同编译环境重新定义u8、s8即可,否则还要关心数据长度、char是否有符号等问题。

SHELL_EXPORT_CMD宏定义错误

SHELL_EXPORT_CMD的宏定义中漏了一个逗号,导致编译错误

#else /** SHELL_LONG_HELP == 1 */
#define     SHELL_EXPORT_CMD(cmd, func, desc)                               \
            const char shellCmd##cmd[] = #cmd;                              \
            const char shellDesc##cmd[] = #desc;                            \
            const SHELL_CommandTypeDef                                      \
            shellCommand##cmd SECTION("shellCommand") =                     \
            {                                                               \
                shellCmd##cmd    (<----此处漏了逗号)                                           \
                (int (*)())func,                                            \
                shellDesc##cmd                                              \
            }

关于SHELL_TypeDef的status中的位域定义

平台:TMS320C6748
编译器:TI C6000 v7.4.4
struct
{
char inputMode : 2; /< 输入模式 */
char tabFlag : 1; /
< tab标志 */
char authFlag : 1; /< 密码标志 */
} status; /
< shell状态 */
对于tagFlag和authFlag编译器会提示警告“#109-D signed bit field of length 1”,也就是定义了一个1bit的有符号数据,也就是说只有一个符号位。
而对于inputMode,相当于一个2bit有符号数,在完成shell->status.inputMode = SHELL_ANSI_CSI;之后,(unsigned char)(shell->status.inputMode)的值为-2,shellAnsi函数永远无法正确执行了,shell挂死。

测试场景,按上下键可以触发。

修改方式:
struct
{
unsigned char inputMode : 2; /< 输入模式 */
unsigned char tabFlag : 1; /
< tab标志 */
unsigned char authFlag : 1; /**< 密码标志 */
} status;

@NevermindZZT

关于定义在shell.c文件外的命令不识别的问题

Hi NevermindZZT,
我使用的是1.8的版本,为了使文件结构看起来更舒服,我把新增加的命令从shell.c文件移到了单独的一个.c文件,但发现使用help命令无法识别这些命令,直接执行也提示找不到命令。
由于我没能理解SHELL_EXPORT_CMD命令是怎么运作的,不知道如何修改,还请帮忙指导,多谢!

移植完成后,只能显示输入的数据,无法进入函数

移植到STM32,使用keil编译程序。接收和发送没有问题,也初始化函数了,在主循环中调用shellTask(&shell); 配置头文件如下,其他未修改,按照freertos的demo文件来的,实际是裸机运行
#define SHELL_TASK_WHILE 0 #define SHELL_USING_CMD_EXPORT 1
例程时使用的readme.md的普通C函数形式,输入letter:/$ func 666 'A' "hello world",屏幕显示letter:/$ func 666 'A' "hello world"。
请问这是哪里没有配置正确?

main函数形式和普通C函数形式合并的思路

建议普通C函数形式main函数形式和普通C函数形式合并,大体思路如下:
1.参照导出变量的方式在在导出的结构体上增加传参模式的参数选项
2.参照变量的结构体封装方式,让用户在导出命令时直接就选择好函数形式即可,比如main函数的用SHELL_EXPORT_CMD(XXX),C函数的用SHELL_EXPORT_CMD_EX(XXX)

关于在shell中传递AT指令参数时不合理的参数解析

使用shell做AT调试时发现shell中使用,和"字符会被认定为参数分隔符,不知道作者在设计的时候这个是基于什么考虑,但是个人认为貌似不太合理,比如传递一个
AT AT+CIPOPEN=0,"114.114.114.114",1883,4001
此类参数时,shell自动分解成多个参数,如

AT 
AT+CIPOPEN=0
114.114.114.114
1883
4001

这就曲解了原本参数传递的意义,一次建议使用空格做分割符而不是,和“,基于原代码shellEnter函数我做了如下修改

for (unsigned short i = 0; i < shell->length; i++)
  {
      if ((quotes != 0 ||
          (*(shell->buffer + i) != ' ' &&
          *(shell->buffer + i) != '\t')) &&
       //   *(shell->buffer + i) != ',' &&
          *(shell->buffer + i) != 0)
      {
//            if (*(shell->buffer + i) == '\"')
//            {
//                quotes = quotes ? 0 : 1;
//            #if SHELL_AUTO_PRASE == 0
//                *(shell->buffer + i) = 0;
//                continue;
//            #endif
//            }
          if (record == 1)
          {
              shell->param[paramCount++] = shell->buffer + i;
              record = 0;
          }
          if (*(shell->buffer + i) == '\\' &&
              *(shell->buffer + i) != 0)
          {
              i++;
          }
      }

建议添加CTRL+C,CTRL+L两个默认快捷键

/**

  • @brief shell 快捷键CTRL+L清空控制台(shell调用)
    */
    void shellKeyClear(Shell *shell)
    {
    shellWriteString(shell, shellText[SHELL_TEXT_CLEAR_CONSOLE]);
    shellWriteCommandLine(shell);
    }
    SHELL_EXPORT_KEY(SHELL_CMD_PERMISSION(0)|SHELL_CMD_ENABLE_UNCHECKED,
    0x0C000000, shellKeyClear, Ctrl+L);

。。。

移植过程中,keil报跟结构体相关的错误

作者您好,看了您的shell十分强大,也想移植一份自己用。但是因为自己不是很熟悉软件,keil中有一报错与结构体有关,四处搜寻无果后无奈寻求帮助。

image
从报错信息来看,似乎是定义和声明的时候在参数里出现了数据格式,参数数量或形参名字的不一致导致的
image
但是无奈不知道如何修改此问题,希望高人能指点一下,谢谢了!

hexdump 打印的部分处理异常,导致重复打印到buff,并且容易超出长度,如下内容是修改后的

void logHexDump(Log *log, void *base, unsigned int length)
{
unsigned char *address;
unsigned int len = length;
unsigned int printLen = 0;

if (length == 0)
{
    return;
}

logWrite(log, LOG_NONE, "memory of 0x%08x, size: %d:\r\n", (unsigned int)base, length);

address = (unsigned char *)((unsigned int)base & (~0x0000000F));
length += (unsigned int)base - (unsigned int)address;
length = (length + 15) & (~0x0000000F);

logWrite(log, LOG_NONE, memPrintHead"\r\n");

while (length)
{
    printLen += sprintf(logBuffer + printLen, memPrintAddr, (unsigned int)address);
    for (int i = 0; i < 16; i++)
    {
        if ((unsigned int)(address + i) < (unsigned int)base
            || (unsigned int)(address + i) >= (unsigned int)base + len)
        {
            logBuffer[printLen ++] = ' ';
            logBuffer[printLen ++] = ' ';
            logBuffer[printLen ++] = ' ';
        }
        else
        {
            printLen += sprintf(logBuffer + printLen, "%02x ", *(address + i));
        }
    }
    logBuffer[printLen ++] = '|';
    logBuffer[printLen ++] = ' ';
    for (int i = 0; i < 16; i++)
    {
        if ((unsigned int)(address + i) < (unsigned int)base
            || (unsigned int)(address + i) >= (unsigned int)base + len)
        {
            logBuffer[printLen ++] = ' ';
        }
        else
        {
            if (*(address + i) >= 32 && *(address + i) <= 126)
            {
                printLen += sprintf(logBuffer + printLen, "%c", *(address + i));
            }
            else
            {
                logBuffer[printLen ++] = '.';
            }
        }
    }
    logBuffer[printLen ++] = '|';
    logBuffer[printLen ++] = '\r';
    logBuffer[printLen ++] = '\n';
    logWriteBuffer(log, LOG_NONE, logBuffer, printLen);
    address += 16;
    length -= 16;
    **printLen = 0;**
}

}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),
hexdump, logHexDump, hex dump\r\n hexdump [log] [base] [len]);

长帮助的处理建议

建议将长帮助和描述合并成一个参数,思路如下:
1.常规显示时,描述超出一定长度后,后面部分截断,显示为"..."
2.如需显示完整的的描述,使用help cmd 的指令模式

合并后整体程序处理会简单不少,而且感觉这样使用起来会更符合实际使用的习惯

看了README.md 不是很理解,请问如何添加命令实现功能?

比如我要实现串口发送一个数据 ,
实现功能函数已写好,命令使用 "send".
void usart_send(void){
}

我应该如何操作?

我使用main函数形式,但是编译报错:section attribute cannot be specified for local variables

func(int argc, char *agrv[])
{
printf("%dparameter(s)\r\n", argc);
for (char i = 1; i < argc; i++)
{
printf("%s\r\n", argv[i]);
}
}
SHELL_EXPORT_CMD(func, func, test)

V2.0.1新增功能请教

在跟进大神的V2.0.1,有些问题和建议:
1、新增的SHELL_EXPORT_CMD_EX(cmd, func, desc, help)导出方式,就是最后增加了一个长描述,这个长描述默认可以被help命令解析并显示出来?
2、SHELL_GET_TICK()这个宏定义需要用户自己额外实现吧?建议shellRead及shellWrite那样,在结构体中用函数指针来实现,这样用户在shell实例化的过程统一处理比较好。

tab键的功能调整建议

2.0.0中tab的功能做了调整,不是直接补齐命令,而是先显示可匹配的命令清单,在进行补全,是否能做一下小的调整:如果可匹配的命令只有一个的时候,直接补全命令,如果有多个可匹配命令时才列出命令清单

使用其他串口助手无法正确显示

image

上图是配置触发,两种方式都试过,依然出现以下情况,使用了示例函数
image

但是使用Xshell工具就没有这个情况,是否因为使用的串口助手问题?那该如何兼容其他串口助手呢?不要求特殊键值,只需发送和接收

密码的问题

密码的使用上感觉有几个问题:
1.输入密码的时候是明文显示的,是否可以改成显示*(即在未正确输入密码前,回显全部都是*即可)
2.密码不是应该作为一个独立的变量加入到SHELL_TypeDef中?要不然所有用户都是同一个密码,体现不出支持多shell的意义(可以把宏密码设定为默认密码,如果初始化前为提前配置密码则按默认密码处理)
3.既然设定了密码,跟着来的就是权限问题了,是否考虑导出密码时就设定命令的权限,然后输入不同的密码可以获得不同的权限,来使用不同的命令?(用户名是否添加感觉不是刚性需求,能区分权限就已经能非常好的满足工程化的需求了)

setVar 指令解析错误

setVar 这个指令必须配合SHELL_AUTO_PRASE来使用才正常,否则就无法解析,建议用宏定义区分开两种不同传参方式

另外shell内部集成的 Help 需要SHELL_AUTO_PRASE = 0 ,和 setVar 刚好相反,这样也就意味着无论SHELL_AUTO_PRASE 如何设置,都有会部分功能缺失

自动参数转化的问题

2.0.0版本中,提供了自动参数转化的功能,但启用后,有几个问题:
1、启用后,被调用的函数,无法再获得argc这个参数,也就不知道命令中输入的参数数量是多少,参数输入少了,判断不出来。
2、隐含的最大参数不能超过8个,也在一定程度上隐含的参数的输入顺序。

建议:
1、2.0.0中的自动参数转换,和之前的参数转换能同时提供给用户使用
2、shellExtParsePara()这个函数开放给被调用函数使用

请问Linux环境下GCC编译器,支持尾行模式吗?

Log uartLog;

void shellLogWrite(char *buffer, short len)
{
    if (uartLog.shell)
    {
        shellWriteEndLine(uartLog.shell, buffer, len);
    }
}

static void *thread1(void *);
static void *thread2(void *);

int main(int argc, char **argv)
{
    printf("enter main\n");
    pthread_t tid1, tid2;
    int rc1 = 0, rc2 = 0;
    rc2 = pthread_create(&tid2, NULL, thread2, NULL);
    if (rc2 != 0)
        printf("%s: %d\n", __func__, strerror(rc2));

    rc1 = pthread_create(&tid1, NULL, thread1, &tid2);
    if (rc1 != 0)
        printf("%s: %d\n", __func__, strerror(rc1));

    while (1);
}

void *thread1(void *arg)
{
    uartLog.write = shellLogWrite;
    uartLog.active = 1;
    uartLog.level = LOG_DEBUG;
    userShellInit();
    logRegister(&uartLog, &shell);
    shellTask(&shell);
}

void *thread2(void *arg)
{
    while(1)
    {
        logInfo("AlarmClockUpdate");
        sleep(1);
    }
}

SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0) | SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC) | SHELL_CMD_PARAM_NUM(1),
                 exit, exit, exit);
SHELL_EXPORT_KEY_AGENCY(SHELL_CMD_PERMISSION(0),
                        0x03000000, exit, exit, 0);

readme疑问

readme的说明第一条
对于中断方式使用shell,不用定义shell->read,但需要在中断中调用shellHandler

但是在shellTask()函数中就有这样一段代码

if` (shell->read == NULL)
    {
        shellDisplay(shell, "error: shell.read must be defined\r\n");
        while (1) ;
    }

不知道这里怎么理解

关于--keep shellCommand的问题

大神,您好,请教一个问题:
在这个shell中建议用keil编译的时候加入--keep shellCommand的选项,我在keil5.26上测试,没有加这个选项,但依然能正常编译(代码优化从0~3都测试过),但给同事使用的时候就必须加上这个选项,请问在什么情况下会出现数据被优化掉?
另外在网上看到一篇文章(https://blog.csdn.net/sinat_33611142/article/details/52761220)说,将__arrtibute__((unused, secition("arm_cmd")))改为__arrtibute__((used, secition("arm_cmd"))),可以避免这个声明,不知道是否有效。

命令函数参数问题

命令函数的参数和终端里面输入的参数的对应关系是什么?比如定义如下函数:
void shellEepromRead(u16 addr)
{
printf("%d\r\n", addr); //如:串口终端输入65580(超出u16范围),这里依然能够正常打印,
//但是定义的是u16,范围是0 ~ 65535才对。
}
其中,u16即无符号16bit的整型,但是实际使用时发现,参数addr的范围可以超过65535,即可以超过u16类型可表示数的范围。

使用的开发环境是MDK5。

如何定义一个 SHELL_CommandTypeDef

我的定义如下:
void func(int i, char ch, char *str)
{
printf("input int: %d, char: %c, string: %s\r\n", i, ch, str);
}

SHELL_CommandTypeDef test = {
"test",
func,
"test com",
"test help"
};
SHELL_EXPORT_CMD(func, func, test);
但是报错:Error section attribute cannot be specified for local variables

获取当前shell的问题

作者在3.0的版本中提供了一个Shell* shellGetCurrent(void)的函数用户获得当前活动的shell,在多进程的情况下,有可能激活的函数有多个,这样的话,此函数的返回值有可能存在错误

ps:
本来一直都没有用到多shell的情况,但是本次改版后shellExtParsePara()这个函数需要提供被调用的shell的信息后,才关注到这个问题.
这个地方其实有点蛋疼.之前我都是用main函数的方式调用,参数解析部分使用shellExtParsePara()来处理,但是这次改版后,需要提供被哪个shell调用,这个地方有点蛋疼了,按理说被调用的函数,是不可能知道被哪个shell来调用了,所以这里只能间接的使用shellGetCurrent()来解决.但执行看代码后发现,此处还是有一点点风险的,虽说是概率不大,但是用在产品上,是绝对会碰到的!

新功能建议

大神,提几个新功能建议,希望能集成到当前shell中(如果不集成也请指导一下如何实现):
1、增加登录密码,当前版本没有登录密码,只能自己测试用,产品投放现场后就不敢用了,希望能增加登录密码的设置。
2、增加变量查询的功能,便于在调试解决查看一些地址的异常情况。这个不知道如何实现比较好,最好能查结构体程序的值。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.