I asked ChatGPT to convert a PHP program to Go, then to Pascal and then to a customised dialect of Basic, it did a pretty good job at Go and Pascal, but what surprised me was how it managed to do the customed Basic dialect pretty good too, it’s not perfect, there is no PostgreSQL or YAML support, but other than that it’s structurally correct, although could do with some cleaup.
It used all the new features I told it about, so pretty amazed.
' gbackup — structured BASIC dialect (labels, BLOCK/SUB/CALL, IF/SWITCH, RECORD)
' Mirrors the Pascal version’s control flow with external tools (cp, rsync, mount, ln)
' NOTE: adjust SHELL commands/paths for your OS/environment.
' ----------------------------- RECORD declarations -----------------------------
RECORD Job LEN = 512
Id AS STRING * 64
Source AS STRING * 200
Dest AS STRING * 200
Mount AS STRING * 32
Copies AS STRING * 4 ' store numeric text like "7"
Enabled AS STRING * 1 ' "1" or "0"
END RECORD
RECORD Exclude LEN = 256
Path AS STRING * 256
END RECORD
' ----------------------------- Globals / settings ------------------------------
' Lock & config
LockFile$ = "/var/run/gbackup.pid"
ConfigFile$ = "/etc/gbackup.yaml" ' not parsed here; stubbed loader below
' Current job (single-job stub; extend to iterate a file/list)
Job_Id$ = ""
Job_Source$ = ""
Job_Dest$ = ""
Job_Mount$ = ""
Job_Copies$ = "7" ' default keep
Job_Enabled$ = "1"
' Scratch
TmpDir$ = "/tmp"
ExcludesFile$ = TmpDir$ + "/gbackup.excludes"
Day$ = "YYYYMMDD-1" ' <-- set via CALL ComputeYesterday or replace with real value
RsyncOpts$ = "-aru --delete-after --delete-excluded --stats"
' Mode controls for SWITCH demo (debug/safe/normal)
Mode$ = "normal"
' ----------------------------- Program entry -----------------------------------
PRINT "gbackup starting…"
CALL ParseArgs
CALL AcquireLock
GOTO precheck
BLOCK precheck
PRINT "Pre-checks…"
CALL ComputeYesterday
CALL LoadConfig
END BLOCK
main:
CALL BackupJob
GOTO done
done:
PRINT "gbackup complete."
CALL ReleaseLock
' ============================= SUBROUTINES =====================================
SUB ParseArgs
' In classic BASIC there are no argv; keep defaults or load from a small text file if needed.
' For demo we keep LockFile$/ConfigFile$ as-is and RsyncOpts$ default.
END SUB
SUB AcquireLock
' Best-effort: if lock file exists, abort; otherwise create.
SHELL "test -f " + LockFile$ + " && echo 'already running' && exit 3"
SHELL "echo $$ > " + LockFile$
END SUB
SUB ReleaseLock
SHELL "rm -f " + LockFile$
END SUB
SUB ComputeYesterday
' If you have a date utility, compute yesterday in YYYYMMDD.
' GNU date example (Linux):
SHELL "date -d 'yesterday' +%Y%m%d > /tmp/.gbackup_day"
OPEN "/tmp/.gbackup_day" FOR INPUT AS #1
LINE INPUT #1, Day$
CLOSE #1
IF Day$ = "" THEN Day$ = "19700101"
END SUB
SUB LoadConfig
' Stub: set a single job. Replace this with your own loader if desired.
' (You could parse a simple INI or read fixed records with FIELD here.)
Job_Id$ = "daily-home"
Job_Source$ = "/home/"
Job_Dest$ = "/backups/home"
Job_Mount$ = "" ' e.g., "/"
Job_Copies$ = "7"
Job_Enabled$ = "1"
' Excludes file: keep a static list or generate from a small text source.
OPEN ExcludesFile$ FOR OUTPUT AS #1
PRINT #1, "*.tmp"
PRINT #1, "*.cache"
PRINT #1, "/home/*/.cache/"
PRINT #1, "/home/*/.local/share/Trash/"
CLOSE #1
' Optional mode (for SWITCH demo)
Mode$ = "normal"
END SUB
SUB BackupJob
IF Job_Enabled$ <> "1" THEN
PRINT "Job disabled: "; Job_Id$
GOTO after_job
END IF
PRINT "==> Job "; Job_Id$
IF Job_Mount$ <> "" THEN
CALL Remount(Job_Mount$, 1)
END IF
CALL DeleteOldestBeyond(Job_Dest$, VAL(Job_Copies$))
' If dest/current exists, ensure day dir via cp phase; else initialize day dir and mark cp done.
IF DirExists(Job_Dest$ + "/current") THEN
IF NOT DirExists(Job_Dest$ + "/" + Day$) THEN
IF NOT CpPhase() THEN
PRINT "cp phase failed (will retry next run)"
GOTO job_epilogue
END IF
END IF
ELSE
CALL MkdirP(Job_Dest$ + "/" + Day$)
CALL PhaseMark("cp", Day$)
END IF
IF NOT RSyncPhase() THEN
PRINT "rsync phase failed (will retry next run)"
GOTO job_epilogue
END IF
IF DirExists(Job_Dest$ + "/" + Day$) THEN
CALL UpdateCurrentSymlink
END IF
job_epilogue:
IF Job_Mount$ <> "" THEN
CALL Remount(Job_Mount$, 0)
END IF
after_job:
CALL FlushToDisk
END SUB
SUB CpPhase
' Returns via shared flag CpOk%
CpOk% = 0
DayDir$ = Job_Dest$ + "/" + Day$
CurrentDir$ = Job_Dest$ + "/current"
IF PhaseDone("cp", Day$) THEN
CpOk% = 1
RETURN
END IF
IF DirExists(DayDir$) THEN
CALL PhaseMark("cp", Day$)
CpOk% = 1
RETURN
END IF
IF NOT DirExists(CurrentDir$) THEN
CALL MkdirP(DayDir$)
CALL PhaseMark("cp", Day$)
CpOk% = 1
RETURN
END IF
CALL MkdirP(DayDir$)
Cmd$ = "nice -n 19 ionice -c 3 -n 7 cp -ralf " + CurrentDir$ + "/* " + DayDir$
CALL RunLogged(Cmd$, Rc%)
IF Rc% <> 0 THEN RETURN
' Remove symlinks from the copied tree
Cmd$ = "nice -n 19 ionice -c 3 -n 7 find " + DayDir$ + " -type l -delete"
CALL RunLogged(Cmd$, Rc%)
IF Rc% <> 0 THEN RETURN
CALL PhaseMark("cp", Day$)
CpOk% = 1
END SUB
SUB RSyncPhase
' Returns via shared flag RsOk%
RsOk% = 0
IF PhaseDone("rsync", Day$) THEN
RsOk% = 1
RETURN
END IF
DestDir$ = Job_Dest$ + "/" + Day$ + "/"
CALL MkdirP(DestDir$)
' Build and run rsync with retries
Cmd$ = "nice -n 19 ionice -c 3 -n 7 rsync " + RsyncOpts$ + " --exclude-from=" + ExcludesFile$ + " " + Job_Source$ + " " + DestDir$
Attempt% = 0
Rc% = 1
WHILE Attempt% < 3 AND Rc% <> 0
CALL RunLogged(Cmd$, Rc%)
IF Rc% = 0 THEN
CALL PhaseMark("rsync", Day$)
RsOk% = 1
RETURN
END IF
CALL SleepMs(2000 * (Attempt% + 1))
Attempt% = Attempt% + 1
WEND
END SUB
SUB Remount(MountPoint$, Rw%)
IF MountPoint$ = "" THEN RETURN
' best effort show
SHELL "mount " + MountPoint$
IF Rw% = 1 THEN Mode$ = "rw" ELSE Mode$ = "ro"
SHELL "mount -o remount," + Mode$ + " " + MountPoint$
END SUB
SUB UpdateCurrentSymlink
SHELL "ln -sfn " + Day$ + " " + Job_Dest$ + "/current"
END SUB
SUB DeleteOldestBeyond(Dest$, Keep%)
' Deletes oldest subdirs beyond Keep (sorted lexicographically by name)
' Caution: simplistic; adjust for your system.
Cmd$ = "ls -1 " + Dest$ + " | grep -Ev '^(current|\\.gbackup)$' | sort | head -n -" + STR$(Keep%) + " | xargs -r -I{} nice -n 19 ionice -c 3 -n 7 rm -rf " + Dest$ + "/{}"
CALL RunLogged(Cmd$, Rc%)
END SUB
SUB PhaseDone(Phase$, Day$)
PhaseFlag% = 0
Flag$ = Job_Dest$ + "/.gbackup/" + Phase$ + "." + Day$ + ".done"
SHELL "test -f " + Flag$ + " || exit 1"
' If test succeeded, shell exit code 0; we can't read it directly, so also touch a temp marker:
' For portability, just try to open; if fails, assume not done:
OPEN Flag$ FOR INPUT AS #1
IF LOF(1) >= 0 THEN PhaseFlag% = 1
CLOSE #1
END SUB
SUB PhaseMark(Phase$, Day$)
CALL MkdirP(Job_Dest$ + "/.gbackup")
Flag$ = Job_Dest$ + "/.gbackup/" + Phase$ + "." + Day$ + ".done"
OPEN Flag$ FOR OUTPUT AS #1
PRINT #1, "done"
CLOSE #1
END SUB
SUB MkdirP(Path$)
SHELL "mkdir -p " + Path$
END SUB
SUB DirExists(Path$)
' Sets DirOk% via a temporary test; classic BASIC lacks a direct API
SHELL "test -d " + Path$ + " && echo 1 > /tmp/.dir_ok || echo 0 > /tmp/.dir_ok"
OPEN "/tmp/.dir_ok" FOR INPUT AS #1
LINE INPUT #1, T$
CLOSE #1
DirOk% = 0
IF T$ = "1" THEN DirOk% = 1
END SUB
SUB RunLogged(Command$, Rc%)
PRINT Command$
SHELL Command$ + " ; printf ' : rc=[%d]\n' $?"
' Grab last exit code by writing to a file:
SHELL "(" + Command$ + "); printf '%d\n' $? > /tmp/.rc"
OPEN "/tmp/.rc" FOR INPUT AS #1
LINE INPUT #1, T$
CLOSE #1
Rc% = VAL(T$)
END SUB
SUB SleepMs(Ms%)
' crude sleep using POSIX sleep; adjust if unavailable
Secs% = Ms% \ 1000
IF Secs% <= 0 THEN Secs% = 1
SHELL "sleep " + STR$(Secs%)
END SUB
SUB FlushToDisk
SHELL "/bin/sync"
END SUB