Commit | Line | Data |
---|---|---|
74f440ea DP |
1 | /* |
2 | * WCMD - Wine-compatible command line interface. | |
3 | * | |
4 | * (C) 1999 D A Pickles | |
5 | */ | |
6 | ||
7 | /* | |
8 | * FIXME: | |
036a9f79 | 9 | * - No support for pipes |
74f440ea DP |
10 | * - 32-bit limit on file sizes in DIR command |
11 | * - Cannot handle parameters in quotes | |
12 | * - Lots of functionality missing from builtins | |
13 | */ | |
14 | ||
15 | #include "wcmd.h" | |
16 | ||
74f440ea DP |
17 | char *inbuilt[] = {"ATTRIB", "CALL", "CD", "CHDIR", "CLS", "COPY", "CTTY", |
18 | "DATE", "DEL", "DIR", "ECHO", "ERASE", "FOR", "GOTO", | |
19 | "HELP", "IF", "LABEL", "MD", "MKDIR", "MOVE", "PATH", "PAUSE", | |
20 | "PROMPT", "REM", "REN", "RENAME", "RD", "RMDIR", "SET", "SHIFT", | |
21 | "TIME", "TYPE", "VERIFY", "VER", "VOL", "EXIT"}; | |
22 | ||
ebecf502 DP |
23 | HINSTANCE hinst; |
24 | DWORD errorlevel; | |
5f8f4f77 | 25 | int echo_mode = 1, verify_mode = 0; |
74f440ea DP |
26 | char nyi[] = "Not Yet Implemented\n\n"; |
27 | char newline[] = "\n"; | |
ebecf502 | 28 | char version_string[] = "WCMD Version 0.14\n\n"; |
74f440ea DP |
29 | char anykey[] = "Press any key to continue: "; |
30 | char quals[MAX_PATH], param1[MAX_PATH], param2[MAX_PATH]; | |
5f8f4f77 | 31 | BATCH_CONTEXT *context = NULL; |
74f440ea DP |
32 | |
33 | /***************************************************************************** | |
34 | * Main entry point. This is a console application so we have a main() not a | |
35 | * winmain(). | |
36 | */ | |
37 | ||
38 | ||
b4459528 | 39 | int wine_main (int argc, char *argv[]) { |
74f440ea DP |
40 | |
41 | char string[1024], args[MAX_PATH], param[MAX_PATH]; | |
42 | int status, i; | |
43 | DWORD count; | |
44 | HANDLE h; | |
45 | ||
74f440ea DP |
46 | args[0] = param[0] = '\0'; |
47 | if (argc > 1) { | |
48 | for (i=1; i<argc; i++) { | |
49 | if (argv[i][0] == '/') { | |
50 | strcat (args, argv[i]); | |
51 | } | |
52 | else { | |
53 | strcat (param, argv[i]); | |
54 | strcat (param, " "); | |
55 | } | |
56 | } | |
57 | } | |
58 | ||
59 | /* | |
60 | * Allocate a console and set it up. | |
61 | */ | |
62 | ||
63 | status = FreeConsole (); | |
64 | if (!status) WCMD_print_error(); | |
65 | status = AllocConsole(); | |
66 | if (!status) WCMD_print_error(); | |
036a9f79 | 67 | SetConsoleMode (GetStdHandle(STD_INPUT_HANDLE), ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | |
74f440ea DP |
68 | ENABLE_PROCESSED_INPUT); |
69 | ||
70 | /* | |
71 | * Execute any command-line options. | |
72 | */ | |
73 | ||
74 | if (strstr(args, "/q") != NULL) { | |
75 | WCMD_echo ("OFF"); | |
76 | } | |
77 | ||
78 | if (strstr(args, "/c") != NULL) { | |
79 | WCMD_process_command (param); | |
80 | return 0; | |
81 | } | |
82 | ||
83 | if (strstr(args, "/k") != NULL) { | |
84 | WCMD_process_command (param); | |
85 | } | |
86 | ||
87 | /* | |
88 | * If there is an AUTOEXEC.BAT file, try to execute it. | |
89 | */ | |
90 | ||
91 | GetFullPathName ("\\autoexec.bat", sizeof(string), string, NULL); | |
92 | h = CreateFile (string, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); | |
93 | if (h != INVALID_HANDLE_VALUE) { | |
94 | CloseHandle (h); | |
205721e7 PS |
95 | #if 0 |
96 | WCMD_batch (string, " "); | |
97 | #endif | |
74f440ea DP |
98 | } |
99 | ||
100 | /* | |
101 | * Loop forever getting commands and executing them. | |
102 | */ | |
103 | ||
104 | WCMD_version (); | |
105 | while (TRUE) { | |
106 | WCMD_show_prompt (); | |
036a9f79 | 107 | ReadFile (GetStdHandle(STD_INPUT_HANDLE), string, sizeof(string), &count, NULL); |
74f440ea DP |
108 | if (count > 1) { |
109 | string[count-1] = '\0'; /* ReadFile output is not null-terminated! */ | |
110 | if (string[count-2] == '\r') string[count-2] = '\0'; /* Under Windoze we get CRLF! */ | |
111 | if (lstrlen (string) != 0) { | |
112 | WCMD_process_command (string); | |
113 | } | |
114 | } | |
115 | } | |
116 | } | |
117 | ||
118 | ||
119 | /***************************************************************************** | |
120 | * Process one command. If the command is EXIT this routine does not return. | |
121 | * We will recurse through here executing batch files. | |
122 | */ | |
123 | ||
124 | ||
125 | void WCMD_process_command (char *command) { | |
126 | ||
127 | char cmd[1024]; | |
128 | char *p; | |
129 | int status, i; | |
130 | DWORD count; | |
036a9f79 | 131 | HANDLE old_stdin = 0, old_stdout = 0, h; |
a5910f45 | 132 | char *whichcmd; |
74f440ea DP |
133 | |
134 | /* | |
135 | * Throw away constructs we don't support yet | |
136 | */ | |
137 | ||
74f440ea DP |
138 | if (strchr(command,'|') != NULL) { |
139 | WCMD_output ("Pipes not yet implemented\n"); | |
140 | return; | |
141 | } | |
142 | ||
143 | /* | |
144 | * Expand up environment variables. | |
145 | */ | |
146 | ||
147 | status = ExpandEnvironmentStrings (command, cmd, sizeof(cmd)); | |
148 | if (!status) { | |
149 | WCMD_print_error (); | |
150 | return; | |
151 | } | |
152 | ||
153 | /* | |
154 | * Changing default drive has to be handled as a special case. | |
155 | */ | |
156 | ||
157 | if ((cmd[1] == ':') && IsCharAlpha (cmd[0]) && (strlen(cmd) == 2)) { | |
158 | status = SetCurrentDirectory (cmd); | |
159 | if (!status) WCMD_print_error (); | |
160 | return; | |
161 | } | |
a5910f45 JE |
162 | |
163 | /* Dont issue newline WCMD_output (newline); @JED*/ | |
74f440ea | 164 | |
036a9f79 DP |
165 | /* |
166 | * Redirect stdin and/or stdout if required. | |
167 | */ | |
168 | ||
169 | if ((p = strchr(cmd,'<')) != NULL) { | |
170 | h = CreateFile (WCMD_parameter (++p, 0, NULL), GENERIC_READ, 0, NULL, OPEN_EXISTING, | |
171 | FILE_ATTRIBUTE_NORMAL, 0); | |
172 | if (h == INVALID_HANDLE_VALUE) { | |
173 | WCMD_print_error (); | |
174 | return; | |
175 | } | |
176 | old_stdin = GetStdHandle (STD_INPUT_HANDLE); | |
177 | SetStdHandle (STD_INPUT_HANDLE, h); | |
178 | } | |
179 | if ((p = strchr(cmd,'>')) != NULL) { | |
180 | h = CreateFile (WCMD_parameter (++p, 0, NULL), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, | |
181 | FILE_ATTRIBUTE_NORMAL, 0); | |
182 | if (h == INVALID_HANDLE_VALUE) { | |
183 | WCMD_print_error (); | |
184 | return; | |
185 | } | |
186 | old_stdout = GetStdHandle (STD_OUTPUT_HANDLE); | |
187 | SetStdHandle (STD_OUTPUT_HANDLE, h); | |
188 | *--p = '\0'; | |
189 | } | |
190 | if ((p = strchr(cmd,'<')) != NULL) *p = '\0'; | |
191 | ||
a5910f45 JE |
192 | /* |
193 | * Strip leading whitespaces, and a '@' if supplied | |
194 | */ | |
195 | whichcmd = WCMD_strtrim_leading_spaces(cmd); | |
196 | if (whichcmd[0] == '@') whichcmd++; | |
197 | ||
74f440ea DP |
198 | /* |
199 | * Check if the command entered is internal. If it is, pass the rest of the | |
200 | * line down to the command. If not try to run a program. | |
201 | */ | |
202 | ||
203 | count = 0; | |
a5910f45 | 204 | while (IsCharAlphaNumeric(whichcmd[count])) { |
74f440ea DP |
205 | count++; |
206 | } | |
207 | for (i=0; i<=WCMD_EXIT; i++) { | |
208 | if (CompareString (LOCALE_USER_DEFAULT, NORM_IGNORECASE | SORT_STRINGSORT, | |
a5910f45 | 209 | whichcmd, count, inbuilt[i], -1) == 2) break; |
74f440ea | 210 | } |
a5910f45 | 211 | p = WCMD_strtrim_leading_spaces (&whichcmd[count]); |
74f440ea DP |
212 | WCMD_parse (p, quals, param1, param2); |
213 | switch (i) { | |
214 | ||
215 | case WCMD_ATTRIB: | |
216 | WCMD_setshow_attrib (); | |
217 | break; | |
218 | case WCMD_CALL: | |
5f8f4f77 | 219 | WCMD_batch (param1, p, 1); |
74f440ea DP |
220 | break; |
221 | case WCMD_CD: | |
222 | case WCMD_CHDIR: | |
223 | WCMD_setshow_default (); | |
224 | break; | |
225 | case WCMD_CLS: | |
226 | WCMD_clear_screen (); | |
227 | break; | |
228 | case WCMD_COPY: | |
229 | WCMD_copy (); | |
230 | break; | |
231 | case WCMD_CTTY: | |
232 | WCMD_change_tty (); | |
233 | break; | |
234 | case WCMD_DATE: | |
235 | WCMD_setshow_date (); | |
236 | break; | |
237 | case WCMD_DEL: | |
238 | case WCMD_ERASE: | |
239 | WCMD_delete (0); | |
240 | break; | |
241 | case WCMD_DIR: | |
242 | WCMD_directory (); | |
243 | break; | |
244 | case WCMD_ECHO: | |
a5910f45 JE |
245 | /* Use the unstripped version of the following data - step over the space */ |
246 | /* but only if a parameter follows */ | |
247 | if (strlen(&whichcmd[count]) > 0) | |
248 | WCMD_echo(&whichcmd[count+1]); | |
249 | else | |
250 | WCMD_echo(&whichcmd[count]); | |
251 | break; | |
74f440ea | 252 | case WCMD_FOR: |
5f8f4f77 | 253 | WCMD_for (p); |
74f440ea DP |
254 | break; |
255 | case WCMD_GOTO: | |
5f8f4f77 | 256 | WCMD_goto (); |
74f440ea DP |
257 | break; |
258 | case WCMD_HELP: | |
259 | WCMD_give_help (p); | |
260 | break; | |
261 | case WCMD_IF: | |
036a9f79 | 262 | WCMD_if (p); |
74f440ea DP |
263 | break; |
264 | case WCMD_LABEL: | |
265 | WCMD_volume (1, p); | |
266 | break; | |
267 | case WCMD_MD: | |
268 | case WCMD_MKDIR: | |
269 | WCMD_create_dir (); | |
270 | break; | |
271 | case WCMD_MOVE: | |
272 | WCMD_move (); | |
273 | break; | |
274 | case WCMD_PATH: | |
275 | WCMD_setshow_path (); | |
276 | break; | |
277 | case WCMD_PAUSE: | |
278 | WCMD_pause (); | |
279 | break; | |
280 | case WCMD_PROMPT: | |
281 | WCMD_setshow_prompt (); | |
282 | break; | |
283 | case WCMD_REM: | |
284 | break; | |
285 | case WCMD_REN: | |
286 | case WCMD_RENAME: | |
287 | WCMD_rename (); | |
288 | break; | |
289 | case WCMD_RD: | |
290 | case WCMD_RMDIR: | |
291 | WCMD_remove_dir (); | |
292 | break; | |
293 | case WCMD_SET: | |
294 | WCMD_setshow_env (p); | |
295 | break; | |
296 | case WCMD_SHIFT: | |
297 | WCMD_shift (); | |
298 | break; | |
299 | case WCMD_TIME: | |
300 | WCMD_setshow_time (); | |
301 | break; | |
302 | case WCMD_TYPE: | |
303 | WCMD_type (); | |
304 | break; | |
305 | case WCMD_VER: | |
306 | WCMD_version (); | |
307 | break; | |
308 | case WCMD_VERIFY: | |
5f8f4f77 | 309 | WCMD_verify (p); |
74f440ea DP |
310 | break; |
311 | case WCMD_VOL: | |
312 | WCMD_volume (0, p); | |
313 | break; | |
314 | case WCMD_EXIT: | |
315 | ExitProcess (0); | |
316 | default: | |
a5910f45 | 317 | WCMD_run_program (whichcmd); |
74f440ea | 318 | }; |
036a9f79 DP |
319 | if (old_stdin) { |
320 | CloseHandle (GetStdHandle (STD_INPUT_HANDLE)); | |
321 | SetStdHandle (STD_INPUT_HANDLE, old_stdin); | |
322 | } | |
323 | if (old_stdout) { | |
324 | CloseHandle (GetStdHandle (STD_OUTPUT_HANDLE)); | |
325 | SetStdHandle (STD_OUTPUT_HANDLE, old_stdout); | |
326 | } | |
74f440ea DP |
327 | } |
328 | ||
329 | /****************************************************************************** | |
330 | * WCMD_run_program | |
331 | * | |
332 | * Execute a command line as an external program. If no extension given then | |
333 | * precedence is given to .BAT files. Must allow recursion. | |
334 | * | |
335 | * FIXME: Case sensitivity in suffixes! | |
336 | */ | |
337 | ||
338 | void WCMD_run_program (char *command) { | |
339 | ||
340 | STARTUPINFO st; | |
341 | PROCESS_INFORMATION pe; | |
342 | BOOL status; | |
343 | HANDLE h; | |
344 | char filetorun[MAX_PATH]; | |
345 | ||
346 | WCMD_parse (command, quals, param1, param2); /* Quick way to get the filename */ | |
347 | if (strpbrk (param1, "\\:") == NULL) { /* No explicit path given */ | |
348 | if ((strchr (param1, '.') == NULL) || (strstr (param1, ".bat") != NULL)) { | |
349 | if (SearchPath (NULL, param1, ".bat", sizeof(filetorun), filetorun, NULL)) { | |
5f8f4f77 | 350 | WCMD_batch (filetorun, command, 0); |
74f440ea DP |
351 | return; |
352 | } | |
353 | } | |
354 | } | |
355 | else { /* Explicit path given */ | |
356 | if (strstr (param1, ".bat") != NULL) { | |
5f8f4f77 | 357 | WCMD_batch (param1, command, 0); |
74f440ea DP |
358 | return; |
359 | } | |
360 | if (strchr (param1, '.') == NULL) { | |
361 | strcpy (filetorun, param1); | |
362 | strcat (filetorun, ".bat"); | |
363 | h = CreateFile (filetorun, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); | |
364 | if (h != INVALID_HANDLE_VALUE) { | |
365 | CloseHandle (h); | |
5f8f4f77 | 366 | WCMD_batch (param1, command, 0); |
74f440ea DP |
367 | return; |
368 | } | |
369 | } | |
370 | } | |
371 | ||
372 | /* No batch file found, assume executable */ | |
373 | ||
374 | ZeroMemory (&st, sizeof(STARTUPINFO)); | |
375 | st.cb = sizeof(STARTUPINFO); | |
376 | status = CreateProcess (NULL, command, NULL, NULL, FALSE, | |
377 | 0, NULL, NULL, &st, &pe); | |
378 | if (!status) { | |
379 | WCMD_print_error (); | |
380 | } | |
ebecf502 DP |
381 | GetExitCodeProcess (pe.hProcess, &errorlevel); |
382 | if (errorlevel == STILL_ACTIVE) errorlevel = 0; | |
74f440ea DP |
383 | } |
384 | ||
385 | /****************************************************************************** | |
386 | * WCMD_show_prompt | |
387 | * | |
388 | * Display the prompt on STDout | |
389 | * | |
390 | */ | |
391 | ||
392 | void WCMD_show_prompt () { | |
393 | ||
394 | int status; | |
395 | char out_string[MAX_PATH], curdir[MAX_PATH], prompt_string[MAX_PATH]; | |
396 | char *p, *q; | |
397 | ||
398 | status = GetEnvironmentVariable ("PROMPT", prompt_string, sizeof(prompt_string)); | |
399 | if ((status == 0) || (status > sizeof(prompt_string))) { | |
400 | lstrcpy (prompt_string, "$N$G"); | |
401 | } | |
402 | p = prompt_string; | |
403 | q = out_string; | |
404 | *q = '\0'; | |
405 | while (*p != '\0') { | |
406 | if (*p != '$') { | |
407 | *q++ = *p++; | |
408 | *q = '\0'; | |
409 | } | |
410 | else { | |
411 | p++; | |
412 | switch (toupper(*p)) { | |
413 | case '$': | |
414 | *q++ = '$'; | |
415 | break; | |
416 | case 'B': | |
417 | *q++ = '|'; | |
418 | break; | |
419 | case 'D': | |
420 | GetDateFormat (LOCALE_USER_DEFAULT, DATE_SHORTDATE, NULL, NULL, q, MAX_PATH); | |
421 | while (*q) q++; | |
422 | break; | |
423 | case 'E': | |
424 | *q++ = '\E'; | |
425 | break; | |
426 | case 'G': | |
427 | *q++ = '>'; | |
428 | break; | |
429 | case 'L': | |
430 | *q++ = '<'; | |
431 | break; | |
432 | case 'N': | |
433 | status = GetCurrentDirectory (sizeof(curdir), curdir); | |
434 | if (status) { | |
435 | *q++ = curdir[0]; | |
436 | } | |
437 | break; | |
438 | case 'P': | |
439 | status = GetCurrentDirectory (sizeof(curdir), curdir); | |
440 | if (status) { | |
441 | lstrcat (q, curdir); | |
442 | while (*q) q++; | |
443 | } | |
444 | break; | |
445 | case 'Q': | |
446 | *q++ = '='; | |
447 | break; | |
448 | case 'T': | |
449 | GetTimeFormat (LOCALE_USER_DEFAULT, 0, NULL, NULL, q, MAX_PATH); | |
450 | while (*q) q++; | |
451 | break; | |
452 | case '_': | |
453 | *q++ = '\n'; | |
454 | break; | |
455 | } | |
456 | p++; | |
457 | *q = '\0'; | |
458 | } | |
459 | } | |
460 | WCMD_output (out_string); | |
461 | } | |
462 | ||
463 | /**************************************************************************** | |
464 | * WCMD_print_error | |
465 | * | |
ebecf502 | 466 | * Print the message for GetLastError |
74f440ea DP |
467 | */ |
468 | ||
469 | void WCMD_print_error () { | |
470 | LPVOID lpMsgBuf; | |
471 | DWORD error_code; | |
ebecf502 | 472 | int status; |
74f440ea DP |
473 | |
474 | error_code = GetLastError (); | |
ebecf502 DP |
475 | status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, |
476 | NULL, error_code, 0, (LPTSTR) &lpMsgBuf, 0, NULL); | |
477 | if (!status) { | |
478 | WCMD_output ("FIXME: Cannot display message for error %d, status %d\n", | |
479 | error_code, GetLastError()); | |
480 | return; | |
74f440ea | 481 | } |
ebecf502 DP |
482 | WCMD_output (lpMsgBuf); |
483 | LocalFree ((HLOCAL)lpMsgBuf); | |
484 | WCMD_output (newline); | |
74f440ea DP |
485 | return; |
486 | } | |
487 | ||
488 | /******************************************************************* | |
489 | * WCMD_parse - parse a command into parameters and qualifiers. | |
490 | * | |
491 | * On exit, all qualifiers are concatenated into q, the first string | |
492 | * not beginning with "/" is in p1 and the | |
493 | * second in p2. Any subsequent non-qualifier strings are lost. | |
494 | * Parameters in quotes are handled. | |
495 | */ | |
496 | ||
497 | void WCMD_parse (char *s, char *q, char *p1, char *p2) { | |
498 | ||
499 | int p = 0; | |
500 | ||
501 | *q = *p1 = *p2 = '\0'; | |
502 | while (TRUE) { | |
503 | switch (*s) { | |
504 | case '/': | |
505 | *q++ = *s++; | |
506 | while ((*s != '\0') && (*s != ' ') && *s != '/') { | |
507 | *q++ = toupper (*s++); | |
508 | } | |
509 | *q = '\0'; | |
510 | break; | |
511 | case ' ': | |
512 | s++; | |
513 | break; | |
514 | case '"': | |
515 | s++; | |
516 | while ((*s != '\0') && (*s != '"')) { | |
517 | if (p == 0) *p1++ = *s++; | |
518 | else if (p == 1) *p2++ = *s++; | |
519 | else s++; | |
520 | } | |
521 | if (p == 0) *p1 = '\0'; | |
522 | if (p == 1) *p2 = '\0'; | |
523 | p++; | |
524 | if (*s == '"') s++; | |
525 | break; | |
526 | case '\0': | |
527 | return; | |
528 | default: | |
529 | while ((*s != '\0') && (*s != ' ') && (*s != '/')) { | |
530 | if (p == 0) *p1++ = *s++; | |
531 | else if (p == 1) *p2++ = *s++; | |
532 | else s++; | |
533 | } | |
534 | if (p == 0) *p1 = '\0'; | |
535 | if (p == 1) *p2 = '\0'; | |
536 | p++; | |
537 | } | |
538 | } | |
539 | } | |
540 | ||
541 | /******************************************************************* | |
542 | * WCMD_output - send output to current standard output device. | |
543 | * | |
544 | */ | |
545 | ||
546 | void WCMD_output (char *format, ...) { | |
547 | ||
548 | va_list ap; | |
549 | char string[1024]; | |
550 | DWORD count; | |
551 | ||
552 | va_start(ap,format); | |
553 | vsprintf (string, format, ap); | |
036a9f79 | 554 | WriteFile (GetStdHandle(STD_OUTPUT_HANDLE), string, lstrlen(string), &count, NULL); |
74f440ea DP |
555 | va_end(ap); |
556 | } | |
557 | ||
a5910f45 JE |
558 | /******************************************************************* |
559 | * WCMD_output_asis - send output to current standard output device. | |
560 | * without formatting eg. when message contains '%' | |
561 | */ | |
562 | ||
563 | void WCMD_output_asis (char *message) { | |
564 | DWORD count; | |
565 | WriteFile (GetStdHandle(STD_OUTPUT_HANDLE), message, lstrlen(message), &count, NULL); | |
566 | } | |
567 | ||
568 | ||
74f440ea | 569 | |
ebecf502 DP |
570 | /*************************************************************************** |
571 | * WCMD_strtrim_leading_spaces | |
572 | * | |
573 | * Remove leading spaces from a string. Return a pointer to the first | |
74f440ea DP |
574 | * non-space character. Does not modify the input string |
575 | */ | |
576 | ||
577 | char *WCMD_strtrim_leading_spaces (char *string) { | |
578 | ||
579 | char *ptr; | |
580 | ||
581 | ptr = string; | |
582 | while (*ptr == ' ') ptr++; | |
583 | return ptr; | |
584 | } | |
585 | ||
ebecf502 DP |
586 | /************************************************************************* |
587 | * WCMD_strtrim_trailing_spaces | |
588 | * | |
589 | * Remove trailing spaces from a string. This routine modifies the input | |
74f440ea DP |
590 | * string by placing a null after the last non-space character |
591 | */ | |
592 | ||
593 | void WCMD_strtrim_trailing_spaces (char *string) { | |
594 | ||
595 | char *ptr; | |
596 | ||
597 | ptr = string + lstrlen (string) - 1; | |
598 | while ((*ptr == ' ') && (ptr >= string)) { | |
599 | *ptr = '\0'; | |
600 | ptr--; | |
601 | } | |
602 | } |