【Linux——实现一个简易shell】

黑暗中的我们都没有说话,你只想回家,不想你回家...............................................................

文章目录

前言

一、【shell工作过程】

二、【命令行参数】

2.1、【获取命令行参数】

1、【输出命令行提示符】

2、【输入命令行参数】

2.2、【解析命令行参数】

1、【分割命令行参数】

2、【判断指令类型】

3、【外部命令的执行】

4、【内建命令的执行】

cd命令:

export指令:

echo指令:

5、【实现重定向】

三、【总结说明以及完整代码】

1、【使shell循环工作】

2、【代码中的全局变量说明】

3、【完整代码及效果演示】

总结


前言

shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可,学习了进程的概念及其控制以后,我们也可以上手做一个简易的shell。


一、【shell工作过程】

我们一般见到的shell一般是下面这个样子:

然后我们可以在光标之后输入一些命令来完成相应的操作:

那么我们来想一下,shell是如何进行工作的呢?

首先我们看到的命令行提示符,光标,这些我们只需要做一些打印工作即可。

但是那些命令是如何执行的呢?

我们知道那些命令实际上也是一个个存放在磁盘中的可执行程序,我们通过在shell界面上输入那些命令名,实际上就是将其从磁盘加载到内存进而进行调用,也就是说,我们要在shell进程里创建子进程,然后通过子进程去调用那些命令的可执行程序,才能完成使用命令的操作。

因此shell需要执行的逻辑其实非常简单,其只需循环执行以下步骤:

  1. 获取命令行。
  2. 解析命令行。
  3. 创建子进程。
  4. 替换子进程。
  5. 等待子进程退出。

其中,创建子进程使用fork函数,替换子进程使用exec系列函数,等待子进程使用wait或者waitpid函数。

下面我们具体来看一看。

二、【命令行参数】

2.1、【获取命令行参数】

1、【输出命令行提示符】

我们首先将shell界面中的命令行提示符打印出来,首先我们先来看看这些命令行提示符都是什么:

不难发现这些都是我们shell环境变量中的值,所以我们可以通过函数getenv(),来获取他们具体如下:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define NUM 1024   //用来存放输入的数组大小 //获取用户名
const char* getUsername()
{const char* name=getenv("USER");if(name){return name;}else {return "none";}
}
//获取主机名
const char* getHostname()
{const char* hostname=getenv("HOSTNAME");if(hostname){return hostname;}else {return "none";}
}
//获取工作目录
const char* getCwd()
{const char* cwd=getenv("PWD");if(cwd){return cwd;}else {return "none";}
}
int main()
{char usercommand[NUM];printf("[%s@%s %s]$ ",getUsername(),getHostname(),getCwd());scanf("%s",usercommand);return 0;
}

打印效果:

2、【输入命令行参数】

但是这样编写的代码会存在一些问题:

如果我们用scanf读取输入的命令行参数,只会读到一个不含空白符的字符串,因为scanf会以空白符为结尾。我们可以看看:

所以我们不能用scanf读取命令行参数,可以用fgets:【fgets - C++ Reference】

注意:

puts遇到空字符停止输出,在输出字符串时会自动在字符串末尾(\0)加一个换行符。
gets()丢弃输入中的换行符,puts()在输出中添加换行符。

fgets()保留输入中的换行符,fputs()不在输出中添加换行符。

所以更改如下:

最后我们对其进行封装:

2.2、【解析命令行参数】

1、【分割命令行参数】

用户输入参数(指令)后,shell会对该行参数进行解析,一般会将字符串按空格进行分割,然后分割好的命令行参数就可以作为程序替换函数exec*的参数,从而实现程序替换,并调用对应的命令程序,而我们这里用strtok函数【strtok - C++ Reference】分割字符串。

这里我们直接进行封装:

最终达到下面的效果即可:

命令行提示符、用户与命令行交互、解析参数的功能实现了,现在我们需要根据用户输入的参数(指令),执行程序。但在执行程序之前,我们必须对指令进行判断。

2、【判断指令类型】

我们之前学习过指令,Linux系统的指令一般可以分为两类 :

  • 一类是内建指令(builtin shell command),内部指令是指内建在shell中的指令,但我们执行该类指令时,不需要额外创建进程,所以内部指令执行的效率高,内建命令就是bash自己执行的,类似于自己内部的一个函数【SHELL编程之内建命令 | Zorro’s Linux Book】
  • 另一类是外部指令(external shell command)。外部指令是指非内建于shell的指令,我们执行该类指令时,会额外创建一个进程。

我们可以通过指令type判断一个指令是否是内建指令,type命令来自英文单词“类型”,其功能是用于查看命令类型,如需区分某个命令是Shell内部指令还是外部命令,则可以使用type命令进行查看。

【参考资料】:type命令 – 查看命令类型 – Linux命令大全(手册)

显示出文件路径的一般是外部命令,显示“is a shell builtin”是内建命令,内建命令不是存放在磁盘中的可执行程序,我们不能简单的创建子进程并进行程序替换,来实现内建命令的调用,对于内建命令我们需要作特殊处理。

3、【外部命令的执行】

对于外部命令的执行,我们不需要考虑太多,只需要在我们的shell进程中创建子进程,使用分割好的命令行参数,进行程序替换,从而完成对应命令的调用。

具体如下:

4、【内建命令的执行】

由于内建命令不同于外部命令只进行简单的程序替换即可完成,内建命令需要特殊的处理,所以对于不同的内建命令我们有不同的处理措施,下面我们看几个例子:

cd命令:

我们知道cd + 目标路径,就可以将我们当前的工作目录改为cd命令后面的目标路径,所以我们可以通过chdir系统调用,将当前的工作路径换为“目标路径”

将工作目录改变以后我们可以通过getcwd函数来进行查看:

需要注意的是,这里我们仍然是创建子进程来实现cd命令所以我们会在子进程中使用chdir函数,而chdir只会进程的当前工作目录,只会影响当前进程及其子进程的工作目录,不会影响环境变量中的PWD,PWD 是由 shell 自动维护,而不是由内核直接管理。

对于典型的 shell,PWD 在你使用命令(如 cd)改变目录时会被更新,但是,直接用系统调用 chdir 不会自动更新 PWD。所以我们仅仅使用chdir函数就会出现下面的场景:

所以当我们调用chdir更改当前工作目录后,还要对环境变量PWD进行修改。先通过调用getcwd()获取当前工作目录的绝对路径,并将其存放到临时空间tmp中,之后通过函数sprintf将tmp的值格式化给全局变量cwd,最后再使用函数putenv将全局变量cwd覆盖环境变量PWD,最后就可以完成修改如下:

export指令:

像cd指令一样,一旦我们识别到对应指令为export,我们就要对其进行特殊的处理,我们知道export命令可用于显示或设置环境变量。所以我们可以使用putenv函数来实现:

echo指令:

当我们使用echo指令打印字符串,都没什么问题,可当我们像下面这样使用时:

按理说这里应该会为我们打印PATH环境变量,但是echo好像将其当成了字符串进行输出,所以我们要来解决一下这个问题,与前面一样我们对其进行针对性特殊处理即可:

5、【实现重定向】

在myshell当中添加重定向功能的步骤大致如下:

  1. 对于获取到的命令进行判断,若命令当中包含重定向符号>>>或是<,则该命令需要进行处理。
  2. 设置type变量,type为0表示命令当中包含输出重定向,type为1表示命令当中包含追加重定向,type为2表示命令当中包含输入重定向。
  3. 重定向符号后面的字段标识为目标文件名,若type值为0,则以写的方式打开目标文件;若type值为1,则以追加的方式打开目标文件;若type值为2,则以读的方式打开目标文件。
  4. 若type值为0或者1,则使用dup2接口实现目标文件与标准输出流的重定向;若type值为2,则使用dup2接口实现目标文件与标准输入流的重定向。

请看下面的实现过程:

最后注意每次进行新指令的输入时把我们定义的redir和filename两个全局变量进行清理:

三、【总结说明以及完整代码】

1、【使shell循环工作】

到这里就算把大部分常见的功能实现完全了,但是细心的你一定会发现,我们的shell还存一个致命的缺陷,就是我们好像只能执行一次命令,所以我们要对其进行完善,是我们的shell能够像正常shell一样,能够连续工作:

2、【代码中的全局变量说明】

我们可以看到我们的代码中用了很多全局变量,那么它们有什么作用呢?

我们先来看lastcode,该全局变量表示程序的退出码,这样我们在父进程等待子进程,或者程序退出的时候获得退出信息:

下面再让我们看看enval和cwd:

我们会发现,这两者都是在调用函数putenv时使用的,也就是说当我们需要添加换将变量时需要定义它们两个定义为全局变量,那么是为什么呢?

首先我们假设没有这两个全局变量,而是将enval和cwd定义在了函数中:

我们要知道当进程启动时,会专门开辟一块空间用来存储命令行参数和环境变量,同时用一个字符串指针数组管理这些环境变量,这个管理环境变量的字符串指针数组就叫做环境变量表(char* envrion[]),当我们用putenv新增一个环境变量时,这时环境变量表会分配一个元素,也就是字符串指针指向这个新增的环境变量,这个新增的环境变量并没有添加到专门存储环境变量的内存空间中(也就是environ指向的空间),而是在栈区,这是因为我们使用argv[1],作为路径path,去当作新传入的环境变量(这里在cd指令中path是目标路径,我们要通过改变环境变量来实现命令行提示符中路径部分的持续变化,而在export中path是我们要导入的环境变量),大致如下:

所以我们通过putenv函数将path添加到环境变量表中,实际上是在environ中创建了一个字符串指针指向了path,而path本身存放在栈区中:

而当我们输入重新在命令行输入指令时,就会刷新命令行参数表argv,如果argv可以分割成两个字符串(也就是argv[0],argv[1]均存在),第二个字符串就会覆盖原来的argv[1],“这样环境变量表environ就找不到原来新增的环境变量了因为被覆盖了。

所以,我们要自己维护一个存放环境变量的空间myenv或cwd,将新增的环境变量存放在myenv和cwd中:

这样就不会覆盖了。 (但再添加一个新的环境变量会覆盖旧的环境变量,大家也可以把自己维护的环境变量设置成二维数组的形式,在堆上申请空间)。

3、【完整代码及效果演示】

为了将系统的shell和我们自己写的shell进行区分,我们可以通过之前写进度条的颜色方案为我们的命令行提示符部分进行上色:

同时也应使用Makefile进行管理:

完整代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#define NUM 1024   //用来存放输入的数组大小 
#define SEP " "#define SIZE 64
char cwd[1024];
char enval[1024]; // for test 
int lastcode=0;
//重定向类别
#define NoneRedir   0// 不是重定向
#define OutputRedir 1//输出重定向
#define AppendRedir 2 //追加重定向
#define InputRedir  3//输入重定向
int redir=NoneRedir;//全局变量,redir,用来标识
char* filename=NULL;// 文件名
//获取用户名
const char* getUsername()
{const char* name=getenv("USER");if(name){return name;}else {return "none";}
}
//获取主机名
const char* getHostname()
{const char* hostname=getenv("HOSTNAME");if(hostname){return hostname;}else {return "none";}
}
//获取工作目录
const char* getCwd()
{const char* cwd=getenv("PWD");if(cwd){return cwd;}else {return "none";}
}
//获取命令行参数
int getUserCommand(char *command, int num)
{printf("\033[35;1m[%s@\033[32;1m%s \033[34;1m%s]\033[31;1m %c\033[0m ", getUsername(), getHostname(), getCwd(),'$');char *r = fgets(command, num, stdin); if(r == NULL)return -1;command[strlen(command) - 1] = '\0';return strlen(command);
}
//分割命令行参数
void commandSplit(char *in, char *out[])
{int argc = 0;out[argc++] = strtok(in, SEP);while( out[argc++] = strtok(NULL, SEP));
}
//执行外部命令
int execute(char *argv[])
{//pid_t id = fork();//创建子进程if(id < 0) return -1;//创建失败else if(id == 0) //child{// 程序替换会不会影响曾经的重定向呢??不会!! 为什么?如何理解??int fd = 0;if(redir == InputRedir){fd = open(filename, O_RDONLY); // 差错处理我们不做了dup2(fd, 0);}else if(redir == OutputRedir){fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if(redir == AppendRedir){fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{//do nothing}//程序替换execvp(argv[0], argv); // argv[0]-->ls,argv[1]-->-a,argv[2]-->-l,argv[3]-->NULL exit(1);//子进程结束后使用exit函数退出}else // father{//父进程等待子进程int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}}return 0;
}void cd(const char *path)
{char tmp[1024];//定义临时空间存放当前工作目录getcwd(tmp, sizeof(tmp));//获取当前工作目录printf("当前工作目录:%s\n",tmp);printf("环境变量:%s\n",getenv("PWD"));chdir(path);//改变工作目录getcwd(tmp, sizeof(tmp));//更新工作目录,并将其存放到tmpsprintf(cwd, "PWD=%s", tmp); //使用tmp覆盖环境变量,cwd的值被格式化为"PWD=tmp"putenv(cwd);//覆盖PWD环境变量printf("当前工作目录:%s\n",tmp);printf("环境变量:%s\n",getenv("PWD"));
}char *homepath()
{char *home = getenv("HOME");if(home) return home;else return (char*)".";
}extern  char    **environ;  //使用全局变量environ打印所有环境变量
//内建命令
int doBuildin(char *argv[])
{if(strcmp(argv[0], "cd") == 0)//判断是否是cd命令{char *path = NULL;if(argv[1] == NULL)path=homepath();//cd后为空就设置为家目录elsepath = argv[1];//不为空就设置为目标路径cd(path);//执行cd逻辑return 1;}else if(strcmp(argv[0], "export") == 0){if(argv[1] == NULL) return 1;//若未传入环境变量则返回strcpy(enval, argv[1]);//否则将传入的环境变量拷贝到全局变量enval中putenv(enval); // 使用putenv函数将enval添加到环境变量中return 1;}else if(strcmp(argv[0], "echo") == 0){if(argv[1] == NULL){printf("\n");return 1;}if(*(argv[1]) == '$' && strlen(argv[1]) > 1)//判断是否是类似与$PATH,$?这种参数{ char *val = argv[1]+1; // 对于不同的参数进行不同的处理,$?就要打印退出码if(strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else//其他的使用getenv函数进行获取并打印即可{const char *enval = getenv(val);if(enval) printf("%s\n", enval);else printf("\n");}return 1;}else//不是$*这种参数就当作字符串即可 {printf("%s\n", argv[1]);return 1;}}else if(0){//有其他内建指令,再进行特殊处理}return 0;
}
#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)//这是一个宏函数,作用是跳过空格,里面使用了函数isspace,该函数作用是判断pos位置是否为空格
void checkRedir(char usercommand[], int len)
{// ls -a -l > log.txt// ls -a -l >> log.txtchar *end = usercommand + len - 1;//从后遍历char *start = usercommand;while(end>start){if(*end == '>')//可能是输入重定向,也可能是追加重定向{if(*(end-1) == '>')//输入重定向{*(end-1) = '\0';filename = end+1;SkipSpace(filename);redir = AppendRedir;break;}else//追加重定向{*end = '\0';filename = end+1;SkipSpace(filename);redir = OutputRedir;break;}}else if(*end == '<')//输出重定向{*end = '\0';filename = end+1;SkipSpace(filename); // 如果有空格,就跳过redir = InputRedir;break;}else//说明未找到“<,<<,>”三个中的一个,继续向前遍历{end--;}}
}
int main()
{while(1){redir=NoneRedir;filename=NULL;char usercommand[NUM];char *argv[SIZE];//获取int n = getUserCommand(usercommand, sizeof(usercommand));if(n<=0)//查看getUserCommand函数返回值,n<=0说明其输入的是空串{continue;//空串直接跳过循环,重新输入}//获得字符串以后,首先检查是否进行了重定向//ls -a -l > log.txt ?> "ls -a -l" [redir_type]   "log.txt"checkRedir(usercommand,sizeof(usercommand));//分割字符串commandSplit(usercommand, argv);n = doBuildin(argv);if(n){continue;//n不为0说明为内建命令,就不向下继续执行了。}n= execute(argv);}return 0;
}

演示效果:

总结


到这里我们就完了一个简易shell的实现,其中我们我们使用了,进程替换,字符分割,重定向,以及文件的打开及关闭,甚至包括dup2函数,等等知识,本篇博客到这里也就结束了,希望对你有所帮助!

......................................................................是否沉默就是你的回答,我们都别挣扎,去爱他

                                                                                                          ————《爱我还是他》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/481473.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

理解Linux的select、poll 和 epoll:从原理到应用场景

I/O 多路复用并不是什么新东西&#xff0c;select 早在 1983 年就出现了&#xff0c;poll 在 1997 年&#xff0c;epoll 是 2002 年的产物。面试题总爱问“多路复用多厉害&#xff1f;”其实它就是把轮询的锅甩给了操作系统&#xff0c;而操作系统不过是用 CPU 指令帮你完成事件…

阅读方法论

选择固有缺陷,选项是对比出来的

关于函数式接口和编程的解析和案例实战

文章目录 匿名内部类“匿名”在哪里 函数式编程lambda表达式的条件Supplier使用示例 ConsumeracceptandThen使用场景 FunctionalBiFunctionalTriFunctional 匿名内部类 匿名内部类的学习和使用是实现lambda表达式和函数式编程的基础。是想一下&#xff0c;我们在使用接口中的方…

ChatGPT 网络安全秘籍(二)

第三章&#xff1a;代码分析和安全开发 这一章深入探讨软件开发的复杂过程&#xff0c;关注当今数字世界中的一个关键问题&#xff1a;确保软件系统的安全。随着技术的不断复杂和威胁的不断演变&#xff0c;采用融合了安全考虑的安全软件开发生命周期&#xff08;SSDLC&#x…

学习笔记044——HashMap源码学习2

文章目录 1、HasMap 底层实现2、HashMap 加载顺序 1、HasMap 底层实现 JDK 1.8 HashMap 底层设计涉及到三种不同的数据结构&#xff0c;分别是数组、链表、红黑树。 1、基本的存储是数组&#xff0c;根据 key 值求出一个数组下标&#xff0c;将元素&#xff08;key-value&am…

计算机网络常见面试题总结(上)

计算机网络基础 网络分层模型 OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f; OSI 七层模型 是国际标准化组织提出的一个网络分层模型&#xff0c;其大体结构以及每一层提供的功能如下图所示&#xff1a; 每一层都专注做一件事情&#xff0c;并且每一层都需…

用micropython 操作stm32f4单片机的定时器实现蜂鸣器驱动

import pyb import time # 初始化引脚和定时器通道作为PWM输出 # 注意&#xff1a;这里我们假设您使用的是支持PWM的引脚和定时器 # 在不同的MicroPython板上&#xff0c;支持的引脚和定时器可能不同 # 请查阅您的板的文档以确认正确的引脚和定时器 buzzer_pin pyb.Pin(PD15,…

前端框架Vue3项目实战(基于Vue3实现一个小相册)

下面是是对Vue3操作的一个项目实战 下面代码是html的基本骨架&#xff08;没有任何的功能&#xff09;&#xff1a; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <title>相册</title> <style&…

【英特尔IA-32架构软件开发者开发手册第3卷:系统编程指南】2001年版翻译,2-39

文件下载与邀请翻译者 学习英特尔开发手册&#xff0c;最好手里这个手册文件。原版是PDF文件。点击下方链接了解下载方法。 讲解下载英特尔开发手册的文章 翻译英特尔开发手册&#xff0c;会是一件耗时费力的工作。如果有愿意和我一起来做这件事的&#xff0c;那么&#xff…

群控系统服务端开发模式-应用开发-前端短信配置开发

一、添加视图 在根目录下src文件夹下views文件夹下param文件夹下sms文件夹下&#xff0c;新建index.vue&#xff0c;代码如下 <template><div class"app-container"><div class"filter-container" style"float:left;"><el…

极致性能:19个Vue 项目的优化手段

前言 在前端开发领域&#xff0c;Vue.js 广泛应用于各种类型的项目中。然而&#xff0c;随着项目规模的扩大和用户需求的增加&#xff0c;性能优化的重要性愈发凸显。优化不仅可以提升用户体验&#xff0c;还能显著减少资源消耗&#xff0c;提高应用的响应速度和稳定性。 本文…

基于Java Springboot个人记账之财来财往微信小程序

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 微信…

【maven-5】Maven 项目构建的生命周期:深入理解与应用

1. 生命周期是什么 ​在Maven出现之前&#xff0c;项目构建的生命周期就已经存在&#xff0c;软件开发人员每天都在对项目进行清理&#xff0c;编译&#xff0c;测试及部署。虽然大家都在不停地做构建工作&#xff0c;但公司和公司间&#xff0c;项目和项目间&#xff0c;往往…

LLamafactory API部署与使用异步方式 API 调用优化大模型推理效率

文章目录 背景介绍第三方大模型API 介绍LLamafactory 部署API大模型 API 调用工具类项目开源 背景介绍 第三方大模型API 目前&#xff0c;市面上有许多第三方大模型 API 服务提供商&#xff0c;通过 API 接口向用户提供多样化的服务。这些平台不仅能提供更多类别和类型的模型…

【Python网络爬虫笔记】6- 网络爬虫中的Requests库

一、概述 Requests 是一个用 Python 语言编写的、简洁且功能强大的 HTTP 库。它允许开发者方便地发送各种 HTTP 请求&#xff0c;如 GET、POST、PUT、DELETE 等&#xff0c;并且可以轻松地处理请求的响应。这个库在 Python 生态系统中被广泛使用&#xff0c;无论是简单的网页数…

【AI技术赋能有限元分析应用实践】Abaqus有限元分析到深度学习方法应用全过程——汽车刹车片热力耦合分析

目录 一、项目实现介绍**项目背景****项目目标****项目流程概述****技术融合****项目价值** 二、实现流程**Step 1: 分析问题构建方法&#xff0c;寻找主要分析目标&#xff0c;确定初步目标****Step 2: 使用 Abaqus 完成有限元仿真&#xff0c;后处理并保存数据为 odb 格式***…

【人工智能-科普】深度森林:传统机器学习与深度学习的创新结合

文章目录 深度森林:传统机器学习与深度学习的创新结合一、什么是深度森林?二、深度森林的工作原理1. **特征提取和转换**2. **多层级训练**3. **最终分类**三、深度森林的关键组成部分1. **森林层(Forest Layer)**2. **级联结构(Cascade Structure)**3. **特征增强(Feat…

Netty的内存池机制怎样设计的?

大家好&#xff0c;我是锋哥。今天分享关于【Netty的内存池机制怎样设计的&#xff1f;】面试题。希望对大家有帮助&#xff1b; Netty的内存池机制怎样设计的&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Netty 的内存池机制设计是为了提高性能&…

Postman设置接口关联,实现参数化

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 postman设置接口关联 在实际的接口测试中&#xff0c;后一个接口经常需要用到前一个接口返回的结果&#xff0c; 从而让后一个接口能正常执行&#xff0c;这…

七牛云成功保存但无法显示和访问{“error“:“download token not specified“}

在使用七牛云存储图片时&#xff0c;前端通过链接访问图片时遇到错误&#xff1a; {"error":"download token not specified"} 具体表现为&#xff1a; 后端通过 access_key 和 secret_key 生成了上传和下载的 Token。前端将域名与 res.key 拼接后生成图…