msc_cli.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. /*
  2. * The MIT License (MIT)
  3. *
  4. * Copyright (c) 2019 Ha Thach (tinyusb.org)
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. *
  24. * This file is part of the TinyUSB stack.
  25. */
  26. #include "msc_cli.h"
  27. #include "ctype.h"
  28. #if CFG_TUH_MSC
  29. #include "ff.h"
  30. #include "diskio.h"
  31. //--------------------------------------------------------------------+
  32. // MACRO CONSTANT TYPEDEF
  33. //--------------------------------------------------------------------+
  34. #define CLI_MAX_BUFFER 256
  35. #define CLI_FILE_READ_BUFFER (4*1024)
  36. enum {
  37. ASCII_BACKSPACE = 8,
  38. };
  39. typedef enum
  40. {
  41. CLI_ERROR_NONE = 0,
  42. CLI_ERROR_INVALID_PARA,
  43. CLI_ERROR_INVALID_PATH,
  44. CLI_ERROR_FILE_EXISTED,
  45. CLI_ERROR_FAILED
  46. }cli_error_t;
  47. static char const * const cli_error_message[] =
  48. {
  49. [CLI_ERROR_NONE ] = 0,
  50. [CLI_ERROR_INVALID_PARA ] = "Invalid parameter(s)",
  51. [CLI_ERROR_INVALID_PATH ] = "No such file or directory",
  52. [CLI_ERROR_FILE_EXISTED ] = "file or directory already exists",
  53. [CLI_ERROR_FAILED ] = "failed to execute"
  54. };
  55. //--------------------------------------------------------------------+
  56. // CLI Database definition
  57. //--------------------------------------------------------------------+
  58. // command, function, description
  59. #define CLI_COMMAND_TABLE(ENTRY) \
  60. ENTRY(unknown , cli_cmd_unknown , NULL ) \
  61. ENTRY(help , cli_cmd_help , NULL ) \
  62. ENTRY(cls , cli_cmd_clear , "Clear the screen.\n cls\n" ) \
  63. ENTRY(ls , cli_cmd_list , "List information of the FILEs.\n ls\n" ) \
  64. ENTRY(cd , cli_cmd_changedir, "change the current directory.\n cd a_folder\n" ) \
  65. ENTRY(cat , cli_cmd_cat , "display contents of a file.\n cat a_file.txt\n" ) \
  66. ENTRY(cp , cli_cmd_copy , "Copies one or more files to another location.\n cp a_file.txt dir1/another_file.txt\n cp a_file.txt dir1\n" ) \
  67. ENTRY(mkdir , cli_cmd_mkdir , "Create a DIRECTORY, if it does not already exist.\n mkdir <new folder>\n" ) \
  68. ENTRY(mv , cli_cmd_move , "Rename or move a DIRECTORY or a FILE.\n mv old_name.txt new_name.txt\n mv old_name.txt dir1/new_name.txt\n" ) \
  69. ENTRY(rm , cli_cmd_remove , "Remove (delete) an empty DIRECTORY or FILE.\n rm deleted_name.txt\n rm empty_dir\n" ) \
  70. //--------------------------------------------------------------------+
  71. // Expands the function to have the standard function signature
  72. //--------------------------------------------------------------------+
  73. #define CLI_PROTOTYPE_EXPAND(command, function, description) \
  74. cli_error_t function(char * p_para);
  75. CLI_COMMAND_TABLE(CLI_PROTOTYPE_EXPAND)
  76. //--------------------------------------------------------------------+
  77. // Expand to enum value
  78. //--------------------------------------------------------------------+
  79. #define CLI_ENUM_EXPAND(command, function, description) CLI_CMDTYPE_##command,
  80. typedef enum
  81. {
  82. CLI_COMMAND_TABLE(CLI_ENUM_EXPAND)
  83. CLI_CMDTYPE_COUNT
  84. }cli_cmdtype_t;
  85. //--------------------------------------------------------------------+
  86. // Expand to string table
  87. //--------------------------------------------------------------------+
  88. #define CLI_STRING_EXPAND(command, function, description) #command,
  89. char const* const cli_string_tbl[] =
  90. {
  91. CLI_COMMAND_TABLE(CLI_STRING_EXPAND)
  92. };
  93. //--------------------------------------------------------------------+
  94. // Expand to Description table
  95. //--------------------------------------------------------------------+
  96. #define CLI_DESCRIPTION_EXPAND(command, function, description) description,
  97. char const* const cli_description_tbl[] =
  98. {
  99. CLI_COMMAND_TABLE(CLI_DESCRIPTION_EXPAND)
  100. };
  101. //--------------------------------------------------------------------+
  102. // Expand to Command Lookup Table
  103. //--------------------------------------------------------------------+
  104. #define CMD_LOOKUP_EXPAND(command, function, description)\
  105. [CLI_CMDTYPE_##command] = function,\
  106. typedef cli_error_t (* const cli_cmdfunc_t)(char *);
  107. static cli_cmdfunc_t cli_command_tbl[] =
  108. {
  109. CLI_COMMAND_TABLE(CMD_LOOKUP_EXPAND)
  110. };
  111. //--------------------------------------------------------------------+
  112. // INTERNAL OBJECT & FUNCTION DECLARATION
  113. //--------------------------------------------------------------------+
  114. CFG_TUSB_MEM_SECTION uint8_t fileread_buffer[CLI_FILE_READ_BUFFER];
  115. static char cli_buffer[CLI_MAX_BUFFER];
  116. static char volume_label[20];
  117. static inline void drive_number2letter(char * p_path)
  118. {
  119. if (p_path[1] == ':')
  120. {
  121. p_path[0] = 'E' + p_path[0] - '0' ;
  122. }
  123. }
  124. static inline void drive_letter2number(char * p_path)
  125. {
  126. if (p_path[1] == ':')
  127. {
  128. p_path[0] = p_path[0] - 'E' + '0';
  129. }
  130. }
  131. //--------------------------------------------------------------------+
  132. // IMPLEMENTATION
  133. //--------------------------------------------------------------------+
  134. // NOTES: prompt re-use cli_buffer --> should not be called when cli_buffer has contents
  135. void cli_command_prompt(void)
  136. {
  137. f_getcwd(cli_buffer, CLI_MAX_BUFFER);
  138. drive_number2letter(cli_buffer);
  139. printf("\n%s %s\n$ ",
  140. (volume_label[0] !=0) ? volume_label : "No Label",
  141. cli_buffer);
  142. tu_memclr(cli_buffer, CLI_MAX_BUFFER);
  143. }
  144. void cli_init(void)
  145. {
  146. tu_memclr(cli_buffer, CLI_MAX_BUFFER);
  147. f_getlabel(NULL, volume_label, NULL);
  148. cli_command_prompt();
  149. }
  150. void cli_poll(char ch)
  151. {
  152. if ( isprint(ch) )
  153. { // accumulate & echo
  154. if (strlen(cli_buffer) < CLI_MAX_BUFFER)
  155. {
  156. cli_buffer[ strlen(cli_buffer) ] = ch;
  157. putchar(ch);
  158. }else
  159. {
  160. puts("cli buffer overflows");
  161. tu_memclr(cli_buffer, CLI_MAX_BUFFER);
  162. }
  163. }
  164. else if ( ch == ASCII_BACKSPACE && strlen(cli_buffer))
  165. {
  166. printf(ANSI_CURSOR_BACKWARD(1) ANSI_ERASE_LINE(0) ); // move cursor back & clear to the end of line
  167. cli_buffer[ strlen(cli_buffer)-1 ] = 0;
  168. }
  169. else if ( ch == '\r')
  170. { // execute command
  171. //------------- Separate Command & Parameter -------------//
  172. putchar('\n');
  173. char* p_space = strchr(cli_buffer, ' ');
  174. uint32_t command_len = (p_space == NULL) ? strlen(cli_buffer) : (uint32_t) (p_space - cli_buffer);
  175. char* p_para = (p_space == NULL) ? (cli_buffer+command_len) : (p_space+1); // point to NULL-character or after space
  176. //------------- Find entered command in lookup table & execute it -------------//
  177. uint8_t cmd_id;
  178. for(cmd_id = CLI_CMDTYPE_COUNT - 1; cmd_id > CLI_CMDTYPE_unknown; cmd_id--)
  179. {
  180. if( 0 == strncmp(cli_buffer, cli_string_tbl[cmd_id], command_len) ) break;
  181. }
  182. cli_error_t error = cli_command_tbl[cmd_id]( p_para ); // command execution, (unknown command if cannot find)
  183. if (CLI_ERROR_NONE != error) puts(cli_error_message[error]); // error message output if any
  184. cli_command_prompt(); // print out current path
  185. }
  186. else if (ch=='\t') // \t may be used for auto-complete later
  187. {
  188. }
  189. }
  190. //--------------------------------------------------------------------+
  191. // UNKNOWN Command
  192. //--------------------------------------------------------------------+
  193. cli_error_t cli_cmd_unknown(char * p_para)
  194. {
  195. (void) p_para;
  196. puts("unknown command, please type \"help\" for list of supported commands");
  197. return CLI_ERROR_NONE;
  198. }
  199. //--------------------------------------------------------------------+
  200. // HELP command
  201. //--------------------------------------------------------------------+
  202. cli_error_t cli_cmd_help(char * p_para)
  203. {
  204. (void) p_para;
  205. puts("current supported commands are:");
  206. for(uint8_t cmd_id = CLI_CMDTYPE_help+1; cmd_id < CLI_CMDTYPE_COUNT; cmd_id++)
  207. {
  208. printf("%s\t%s\n", cli_string_tbl[cmd_id], cli_description_tbl[cmd_id]);
  209. }
  210. return CLI_ERROR_NONE;
  211. }
  212. //--------------------------------------------------------------------+
  213. // Clear Screen Command
  214. //--------------------------------------------------------------------+
  215. cli_error_t cli_cmd_clear(char* p_para)
  216. {
  217. (void) p_para;
  218. printf(ANSI_ERASE_SCREEN(2) ANSI_CURSOR_POSITION(1,1) );
  219. return CLI_ERROR_NONE;
  220. }
  221. //--------------------------------------------------------------------+
  222. // LS Command
  223. //--------------------------------------------------------------------+
  224. cli_error_t cli_cmd_list(char * p_para)
  225. {
  226. if ( strlen(p_para) == 0 ) // list current directory
  227. {
  228. DIR target_dir;
  229. if ( FR_OK != f_opendir(&target_dir, ".") ) return CLI_ERROR_FAILED;
  230. TCHAR long_filename[_MAX_LFN];
  231. FILINFO dir_entry =
  232. {
  233. .lfname = long_filename,
  234. .lfsize = _MAX_LFN
  235. };
  236. while( (f_readdir(&target_dir, &dir_entry) == FR_OK) && dir_entry.fname[0] != 0)
  237. {
  238. if ( dir_entry.fname[0] != '.' ) // ignore . and .. entry
  239. {
  240. TCHAR const * const p_name = (dir_entry.lfname[0] != 0) ? dir_entry.lfname : dir_entry.fname;
  241. if ( dir_entry.fattrib & AM_DIR ) // directory
  242. {
  243. printf("/%s", p_name);
  244. }else
  245. {
  246. printf("%-40s%d KB", p_name, dir_entry.fsize / 1000);
  247. }
  248. putchar('\n');
  249. }
  250. }
  251. // (void) f_closedir(&target_dir);
  252. }
  253. else
  254. {
  255. puts("ls only supports list current directory only, try to cd to that folder first");
  256. return CLI_ERROR_INVALID_PARA;
  257. }
  258. return CLI_ERROR_NONE;
  259. }
  260. //--------------------------------------------------------------------+
  261. // CD Command
  262. //--------------------------------------------------------------------+
  263. cli_error_t cli_cmd_changedir(char * p_para)
  264. {
  265. if ( strlen(p_para) == 0 ) return CLI_ERROR_INVALID_PARA;
  266. drive_letter2number(p_para);
  267. if ( FR_OK != f_chdir(p_para) )
  268. {
  269. return CLI_ERROR_INVALID_PATH;
  270. }
  271. if ( p_para[1] == ':')
  272. { // path has drive letter --> change drive, update volume label
  273. f_chdrive(p_para[0] - '0');
  274. f_getlabel(NULL, volume_label, NULL);
  275. }
  276. return CLI_ERROR_NONE;
  277. }
  278. //--------------------------------------------------------------------+
  279. // CAT Command
  280. //--------------------------------------------------------------------+
  281. cli_error_t cli_cmd_cat(char *p_para)
  282. {
  283. if ( strlen(p_para) == 0 ) return CLI_ERROR_INVALID_PARA;
  284. FIL file;
  285. switch( f_open(&file, p_para, FA_READ) )
  286. {
  287. case FR_OK:
  288. {
  289. uint32_t bytes_read = 0;
  290. if ( (FR_OK == f_read(&file, fileread_buffer, CLI_FILE_READ_BUFFER, &bytes_read)) && (bytes_read > 0) )
  291. {
  292. if ( file.fsize < 0x80000 ) // ~ 500KB
  293. {
  294. putchar('\n');
  295. do {
  296. for(uint32_t i=0; i<bytes_read; i++) putchar( fileread_buffer[i] );
  297. }while( (FR_OK == f_read(&file, fileread_buffer, CLI_FILE_READ_BUFFER, &bytes_read)) && (bytes_read > 0) );
  298. }else
  299. { // not display file contents if first character is not printable (high chance of binary file)
  300. printf("%s 's contents is too large\n", p_para);
  301. }
  302. }
  303. f_close(&file);
  304. }
  305. break;
  306. case FR_INVALID_NAME:
  307. return CLI_ERROR_INVALID_PATH;
  308. default :
  309. return CLI_ERROR_FAILED;
  310. }
  311. return CLI_ERROR_NONE;
  312. }
  313. //--------------------------------------------------------------------+
  314. // Make Directory command
  315. //--------------------------------------------------------------------+
  316. cli_error_t cli_cmd_mkdir(char *p_para)
  317. {
  318. if ( strlen(p_para) == 0 ) return CLI_ERROR_INVALID_PARA;
  319. return (f_mkdir(p_para) == FR_OK) ? CLI_ERROR_NONE : CLI_ERROR_FAILED;
  320. }
  321. //--------------------------------------------------------------------+
  322. // COPY command
  323. //--------------------------------------------------------------------+
  324. cli_error_t cli_cmd_copy(char *p_para)
  325. {
  326. char* p_space = strchr(p_para, ' ');
  327. if ( p_space == NULL ) return CLI_ERROR_INVALID_PARA;
  328. *p_space = 0; // replace space by NULL-character
  329. char* p_dest = p_space+1;
  330. if ( strlen(p_dest) == 0 ) return CLI_ERROR_INVALID_PARA;
  331. drive_letter2number(p_para);
  332. drive_letter2number(p_dest);
  333. //------------- Check Existence of source file -------------//
  334. FIL src_file;
  335. if ( FR_OK != f_open(&src_file , p_para, FA_READ) ) return CLI_ERROR_INVALID_PATH;
  336. //------------- Check if dest path is a folder or a non-existing file (overwritten is not allowed) -------------//
  337. FILINFO dest_entry;
  338. if ( (f_stat(p_dest, &dest_entry) == FR_OK) && (dest_entry.fattrib & AM_DIR) )
  339. { // the destination is an existed folder --> auto append dest filename to be the folder
  340. strcat(p_dest, "/");
  341. strcat(p_dest, p_para);
  342. }
  343. //------------- Open dest file and start the copy -------------//
  344. cli_error_t error = CLI_ERROR_NONE;
  345. FIL dest_file;
  346. switch ( f_open(&dest_file, p_dest, FA_WRITE | FA_CREATE_NEW) )
  347. {
  348. case FR_EXIST:
  349. error = CLI_ERROR_FILE_EXISTED;
  350. break;
  351. case FR_OK:
  352. while(1) // copying
  353. {
  354. uint32_t bytes_read = 0;
  355. uint32_t bytes_write = 0;
  356. FRESULT res;
  357. res = f_read(&src_file, fileread_buffer, CLI_FILE_READ_BUFFER, &bytes_read); /* Read a chunk of src file */
  358. if ( (res != FR_OK) || (bytes_read == 0) ) break; /* error or eof */
  359. res = f_write(&dest_file, fileread_buffer, bytes_read, &bytes_write); /* Write it to the dst file */
  360. if ( (res != FR_OK) || (bytes_write < bytes_read) ) break; /* error or disk full */
  361. }
  362. f_close(&dest_file);
  363. break;
  364. default:
  365. error = CLI_ERROR_FAILED;
  366. break;
  367. }
  368. f_close(&src_file);
  369. return error;
  370. }
  371. //--------------------------------------------------------------------+
  372. // MOVE/RENAME
  373. //--------------------------------------------------------------------+
  374. cli_error_t cli_cmd_move(char *p_para)
  375. {
  376. char* p_space = strchr(p_para, ' ');
  377. if ( p_space == NULL ) return CLI_ERROR_INVALID_PARA;
  378. *p_space = 0; // replace space by NULL-character
  379. char* p_dest = p_space+1;
  380. if ( strlen(p_dest) == 0 ) return CLI_ERROR_INVALID_PARA;
  381. return (f_rename(p_para, p_dest) == FR_OK ) ? CLI_ERROR_NONE : CLI_ERROR_FAILED;
  382. }
  383. //--------------------------------------------------------------------+
  384. // REMOVE
  385. //--------------------------------------------------------------------+
  386. cli_error_t cli_cmd_remove(char *p_para)
  387. {
  388. if ( strlen(p_para) == 0 ) return CLI_ERROR_INVALID_PARA;
  389. switch( f_unlink(p_para) )
  390. {
  391. case FR_OK: return CLI_ERROR_NONE;
  392. case FR_DENIED:
  393. printf("cannot remove readonly file/foler or non-empty folder\n");
  394. break;
  395. default: break;
  396. }
  397. return CLI_ERROR_FAILED;
  398. }
  399. #endif