Implement DATE$/TIME$/TIMER, FILES, SHELL, CHDIR, MKDIR, RMDIR
DATE$, TIME$, and TIMER now return real system date/time instead of hardcoded values. Added directory and shell access statements with proper GW-BASIC error codes (Path not found 76, File already exists 60). Bump to v0.6.0, 52 tests.
This commit is contained in:
@@ -4,6 +4,9 @@ project(gw-basic-2026 C)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# POSIX extensions (opendir, chdir, strcasecmp, etc.)
|
||||
add_compile_definitions(_DEFAULT_SOURCE)
|
||||
|
||||
# Warnings
|
||||
add_compile_options(-Wall -Wextra -Wno-unused-parameter)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor:
|
||||
|
||||
```
|
||||
$ ./gwbasic
|
||||
GW-BASIC 2026 0.5.0
|
||||
GW-BASIC 2026 0.6.0
|
||||
(C) Eremey Valetov 2026. MIT License.
|
||||
Based on Microsoft GW-BASIC assembly source.
|
||||
Ok
|
||||
@@ -73,7 +73,8 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$
|
||||
| Program I/O | SAVE, LOAD, MERGE, CHAIN |
|
||||
| Error handling | ON ERROR GOTO, RESUME, ERROR, ERR, ERL |
|
||||
| User functions | DEF FN, RANDOMIZE |
|
||||
| File management | KILL, NAME |
|
||||
| File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL |
|
||||
| Date/time | DATE$, TIME$, TIMER |
|
||||
| Screen | LOCATE, COLOR, WIDTH, SCREEN, KEY ON/OFF/LIST |
|
||||
| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT |
|
||||
| Sound | SOUND, BEEP, PLAY (MML) |
|
||||
@@ -145,7 +146,7 @@ Key design differences from the original:
|
||||
|
||||
## Tests
|
||||
|
||||
50 test programs in `tests/programs/`, with CI via GitHub Actions:
|
||||
52 test programs in `tests/programs/`, with CI via GitHub Actions:
|
||||
|
||||
```bash
|
||||
bash tests/run_tests.sh
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
#define ERR_NM 65 /* Bad file name */
|
||||
#define ERR_DS 67 /* Direct statement in file */
|
||||
#define ERR_TF 68 /* Too many files */
|
||||
#define ERR_DA 70 /* Disk already exists */
|
||||
#define ERR_PE 76 /* Path not found */
|
||||
|
||||
const char *gw_error_msg(int errnum);
|
||||
void gw_error(int errnum);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "gw_math.h"
|
||||
#include "strings.h"
|
||||
|
||||
#define GW_VERSION "0.5.0"
|
||||
#define GW_VERSION "0.6.0"
|
||||
#define GW_BANNER "GW-BASIC " GW_VERSION
|
||||
|
||||
/* Tokenizer */
|
||||
|
||||
@@ -52,6 +52,8 @@ static const struct { int num; const char *msg; } error_table[] = {
|
||||
{ 65, "Bad file name" },
|
||||
{ 67, "Direct statement in file" },
|
||||
{ 68, "Too many files" },
|
||||
{ 70, "Disk already exists" },
|
||||
{ 76, "Path not found" },
|
||||
{ 0, NULL }
|
||||
};
|
||||
|
||||
|
||||
23
src/eval.c
23
src/eval.c
@@ -4,6 +4,7 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
|
||||
/*
|
||||
* Expression evaluator - reimplements GWEVAL.ASM's FRMEVL.
|
||||
@@ -919,7 +920,7 @@ static gw_value_t eval_atom(void)
|
||||
gw.text_ptr = save;
|
||||
}
|
||||
|
||||
/* Extended statement tokens that work as functions (DATE$, TIME$) */
|
||||
/* Extended statement tokens that work as functions (DATE$, TIME$, TIMER) */
|
||||
if (tok == TOK_PREFIX_FE) {
|
||||
uint8_t *save = gw.text_ptr;
|
||||
gw_chrget();
|
||||
@@ -928,13 +929,29 @@ static gw_value_t eval_atom(void)
|
||||
gw_chrget();
|
||||
gw_value_t v;
|
||||
v.type = VT_STR;
|
||||
char tbuf[16];
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm = localtime(&now);
|
||||
if (xtok == XSTMT_DATE) {
|
||||
v.sval = gw_str_from_cstr("01-01-2026");
|
||||
snprintf(tbuf, sizeof(tbuf), "%02d-%02d-%04d",
|
||||
tm->tm_mon + 1, tm->tm_mday, tm->tm_year + 1900);
|
||||
v.sval = gw_str_from_cstr(tbuf);
|
||||
} else {
|
||||
v.sval = gw_str_from_cstr("00:00:00");
|
||||
snprintf(tbuf, sizeof(tbuf), "%02d:%02d:%02d",
|
||||
tm->tm_hour, tm->tm_min, tm->tm_sec);
|
||||
v.sval = gw_str_from_cstr(tbuf);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if (xtok == XSTMT_TIMER) {
|
||||
gw_chrget();
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm = localtime(&now);
|
||||
gw_value_t v;
|
||||
v.type = VT_SNG;
|
||||
v.fval = (float)(tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec);
|
||||
return v;
|
||||
}
|
||||
gw.text_ptr = save;
|
||||
}
|
||||
|
||||
|
||||
130
src/interp.c
130
src/interp.c
@@ -7,6 +7,10 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
/*
|
||||
* Execution loop and control flow - the heart of the interpreter.
|
||||
@@ -15,6 +19,14 @@
|
||||
|
||||
jmp_buf gw_run_jmp;
|
||||
|
||||
static int ci_strcmp(const char *a, const char *b)
|
||||
{
|
||||
for (;; a++, b++) {
|
||||
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
|
||||
if (d != 0 || !*a) return d;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* Program Storage
|
||||
* ================================================================ */
|
||||
@@ -873,6 +885,124 @@ void gw_exec_stmt(void)
|
||||
free(cmd);
|
||||
return;
|
||||
}
|
||||
/* FILES [filespec$] */
|
||||
if (xstmt == XSTMT_FILES) {
|
||||
gw_chrget();
|
||||
gw_skip_spaces();
|
||||
char *pattern = NULL;
|
||||
if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) {
|
||||
gw_value_t v = gw_eval_str();
|
||||
pattern = gw_str_to_cstr(&v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
}
|
||||
const char *dir = ".";
|
||||
char dirpath[256] = ".";
|
||||
const char *wild = NULL;
|
||||
if (pattern) {
|
||||
/* Split "dir/pattern" into directory and wildcard */
|
||||
char *sep = strrchr(pattern, '/');
|
||||
if (sep) {
|
||||
*sep = '\0';
|
||||
snprintf(dirpath, sizeof(dirpath), "%s", pattern);
|
||||
dir = dirpath;
|
||||
wild = sep + 1;
|
||||
} else {
|
||||
wild = pattern;
|
||||
}
|
||||
if (wild && !*wild) wild = NULL;
|
||||
}
|
||||
DIR *dp = opendir(dir);
|
||||
if (!dp) { free(pattern); gw_error(ERR_PE); }
|
||||
struct dirent *ent;
|
||||
int col = 0;
|
||||
while ((ent = readdir(dp)) != NULL) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
if (wild) {
|
||||
/* Simple *.ext matching: if wild starts with *. check extension */
|
||||
if (wild[0] == '*' && wild[1] == '.') {
|
||||
const char *ext = strrchr(ent->d_name, '.');
|
||||
if (!ext || ci_strcmp(ext + 1, wild + 2) != 0) continue;
|
||||
} else if (strcmp(wild, "*.*") != 0 && strcmp(wild, "*") != 0) {
|
||||
/* Exact match */
|
||||
if (ci_strcmp(ent->d_name, wild) != 0) continue;
|
||||
}
|
||||
}
|
||||
char entry[270];
|
||||
snprintf(entry, sizeof(entry), "%-14s", ent->d_name);
|
||||
if (gw_hal) gw_hal->puts(entry);
|
||||
else fputs(entry, stdout);
|
||||
col += 14;
|
||||
if (col >= 70) {
|
||||
if (gw_hal) gw_hal->puts("\n");
|
||||
else fputs("\n", stdout);
|
||||
col = 0;
|
||||
}
|
||||
}
|
||||
if (col > 0) {
|
||||
if (gw_hal) gw_hal->puts("\n");
|
||||
else fputs("\n", stdout);
|
||||
}
|
||||
closedir(dp);
|
||||
free(pattern);
|
||||
return;
|
||||
}
|
||||
/* SHELL [command$] */
|
||||
if (xstmt == XSTMT_SHELL) {
|
||||
gw_chrget();
|
||||
gw_skip_spaces();
|
||||
if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) {
|
||||
gw_value_t v = gw_eval_str();
|
||||
char *cmd = gw_str_to_cstr(&v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
int rc = system(cmd);
|
||||
free(cmd);
|
||||
(void)rc;
|
||||
} else {
|
||||
const char *sh = getenv("SHELL");
|
||||
if (!sh) sh = "/bin/sh";
|
||||
int rc = system(sh);
|
||||
(void)rc;
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* CHDIR path$ */
|
||||
if (xstmt == XSTMT_CHDIR) {
|
||||
gw_chrget();
|
||||
gw_value_t v = gw_eval_str();
|
||||
char *path = gw_str_to_cstr(&v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
if (chdir(path) != 0) { free(path); gw_error(ERR_PE); }
|
||||
free(path);
|
||||
return;
|
||||
}
|
||||
/* MKDIR path$ */
|
||||
if (xstmt == XSTMT_MKDIR) {
|
||||
gw_chrget();
|
||||
gw_value_t v = gw_eval_str();
|
||||
char *path = gw_str_to_cstr(&v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
if (mkdir(path, 0755) != 0) {
|
||||
int e = errno;
|
||||
free(path);
|
||||
gw_error(e == EEXIST ? ERR_FE : ERR_PE);
|
||||
}
|
||||
free(path);
|
||||
return;
|
||||
}
|
||||
/* RMDIR path$ */
|
||||
if (xstmt == XSTMT_RMDIR) {
|
||||
gw_chrget();
|
||||
gw_value_t v = gw_eval_str();
|
||||
char *path = gw_str_to_cstr(&v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
if (rmdir(path) != 0) {
|
||||
int e = errno;
|
||||
free(path);
|
||||
gw_error(e == ENOENT ? ERR_PE : ERR_FF);
|
||||
}
|
||||
free(path);
|
||||
return;
|
||||
}
|
||||
/* Stubs: VIEW, WINDOW, PALETTE */
|
||||
if (xstmt == XSTMT_VIEW ||
|
||||
xstmt == XSTMT_WINDOW || xstmt == XSTMT_PALETTE) {
|
||||
|
||||
15
tests/programs/datetime.bas
Normal file
15
tests/programs/datetime.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
10 REM DATE$, TIME$, TIMER test
|
||||
20 D$ = DATE$
|
||||
30 T$ = TIME$
|
||||
40 R = TIMER
|
||||
50 IF LEN(D$) <> 10 THEN PRINT "FAIL date len": END
|
||||
60 IF MID$(D$,3,1) <> "-" THEN PRINT "FAIL date sep1": END
|
||||
70 IF MID$(D$,6,1) <> "-" THEN PRINT "FAIL date sep2": END
|
||||
80 IF LEN(T$) <> 8 THEN PRINT "FAIL time len": END
|
||||
90 IF MID$(T$,3,1) <> ":" THEN PRINT "FAIL time sep1": END
|
||||
100 IF MID$(T$,6,1) <> ":" THEN PRINT "FAIL time sep2": END
|
||||
110 IF R < 0 OR R > 86400 THEN PRINT "FAIL timer range": END
|
||||
120 PRINT "DATE$ = "; D$
|
||||
130 PRINT "TIME$ = "; T$
|
||||
140 PRINT "TIMER ="; INT(R)
|
||||
150 PRINT "All date/time tests passed"
|
||||
11
tests/programs/filesystem.bas
Normal file
11
tests/programs/filesystem.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM FILES, MKDIR, CHDIR, RMDIR test
|
||||
20 MKDIR "gwb_test_dir"
|
||||
30 OPEN "gwb_test_dir/test.txt" FOR OUTPUT AS #1
|
||||
40 PRINT #1, "hello"
|
||||
50 CLOSE #1
|
||||
60 CHDIR "gwb_test_dir"
|
||||
70 SHELL "pwd > /dev/null"
|
||||
80 CHDIR ".."
|
||||
90 KILL "gwb_test_dir/test.txt"
|
||||
100 RMDIR "gwb_test_dir"
|
||||
110 PRINT "All filesystem tests passed"
|
||||
Reference in New Issue
Block a user