--- /dev/null
- \fB\-x \-\-expunge\fP, Removes what's specified in the expunge block
+.TH BAKE "1" "August 2024" "bake 20240804" "User Commands"
+.SH NAME
+.B bake
+\- file embeddable scripts
+.SH SYNOPSIS
+.B bake
+[\-chlnx] [\-s <N>] <FILENAME> [ARGS...]
+.SH DESCRIPTION
+
+bake is a simple tool meant to execute embedded shell commands within
+any file. It executes with /bin/sh the command after a "\fB@BAKE\fP " to
+the end of the line (a UNIX newline: '\fB\\n\fP').
+
+This format may be embedded within \fBbinary files\fP, or any file where no unwanted preceding
+instance of \fB@BAKE\fP appears.
+
+It roots the shell execution in the directory of the given file.
+
+Options must always be put first, and short options may be merged together, numerical options must be trailing.
+
+.HP
+ \fB\-c \-\-color\fP, Disables color
+ \-h \-\-help, Help message
+\fB\-n \-\-dry\-run\fP, don't execute anything
+\fB\-l \-\-list\fP, lists available shell commands
+\fB\-s \-\-select\fP <\FBn\fP>, selects Nth shell command
- .B @FILENAME, @NAME, $@
++\fB\-x \-\-expunge\fP, Removes what's specified in the expunge block <disabled for this build>
+.PP
+Macros
+
+All macros can be exempted by prefixing them with a backslash,
+which'll be subtracted in the expansion. multi-line commands may be
+done by a leading backslash, which are NOT subtracted.
+
+These macros will expand to their counterpart before execution.
+.TP
++.B @FILE, @FILENAME, @NAME, $@
+returns target\-file (abc.x.txt)
+.TP
+.B @SHORT, $*
+returns target\-file without suffix (abc.x.txt \-> abc.x)
++supports choice syntax, @SHORT:1
+.TP
+.B @ARGS, $+
+returns
+.B arguments
++, supports choice syntax, @ARGS:0 returns the first argument, @ARGS:N so on.
+.TP
+.B @LINE
+returns the line number
+
+.PP
+Additional Features And Notes
+
+Shell execution may be disabled with the \fB-n\fP or \fB--dry-run\fP option.
+
+Expunge removes exactly one file specified in the @{...} format. You may use
+backslashes to remove
+
+.SH EXAMPLE
+.\" SRC BEGIN (example.c)
+.EX
+// example.c @BAKE cc -o @SHORT @NAME @ARGS
+#include <stdio.h>
+int main (void) {
+ puts("Hello.");
+ return 0;
+}
+.EE
+.SH COPYRIGHT
+.PP
+Licensed under the public domain.
--- /dev/null
--- /dev/null
++/* cbake.l @BAKE flex @FILE && cc -Wall -Wextra -std=c99 -D_GNU_SOURCE -o @SHORT lex.yy.c @ARGS -lfl @STOP */
++/* TODO: implement expunge, color */
++%{
++#include <ctype.h>
++
++#undef ECHO
++#define ECHO do { fprintf(stdout, yytext); if (g_pipe) { fprintf(g_pipe, yytext); } } while (0)
++#define CHAR(c) do { fputc(c, stdout); if (g_pipe) { fputc(c, g_pipe); } } while (0)
++#define STRING(s) do { fputs(s, stdout); if (g_pipe) { fputs(s, g_pipe); } } while (0)
++#define FORMAT(...) do { fprintf(stdout, __VA_ARGS__); if (g_pipe) { fprintf(g_pipe, __VA_ARGS__); } } while (0)
++#define FWRITE(str, len) do { fwrite(str, 1, len, stdout); if (g_pipe) { fwrite(str, 1, len, g_pipe); } } while (0)
++
++/* input from main to lexer */
++FILE * g_pipe;
++char * g_filename;
++int g_ac;
++char ** g_av;
++int g_select = 1;
++/* for the lexers eyes only */
++int line = 1, nth = 0, expunge_depth = 0, first_nl, tmpline;
++
++extern void root(char * filename);
++extern void args(int n);
++extern void shorten(char * filename, int n);
++%}
++
++SPACE [ \t\r\v\f]
++MACROS (@BAKE|@FILENAME|@FILE|@NAME|@SHORT|@ARGS|@LINE|@STOP|$@|$*|$+)
++
++%x FOUND PADDING STOP
++%option nodefault noinput nounput noyywrap
++%%
++
++@BAKE[[:space:]] { bake:
++ first_nl = 1;
++
++ if (yytext[yyleng-1] == '\n') { ++line; }
++ if (!g_select) { ; }
++ else if (g_select < 0) { BEGIN FOUND; printf("\n%s:%d:s%d: ", g_filename, line, ++nth); }
++ else if (!--g_select) { BEGIN FOUND; }
++}
++
++\n { ++line; }
++. {;}
++
++<FOUND>{
++ @BAKE[[:space:]]|@STOP { BEGIN INITIAL; yyless(0); if (first_nl) { CHAR('\n'); } if (!g_select) { return 0; } }
++ @FILENAME|@FILE|@NAME|$@ { STRING(g_filename); }
++ @SHORT:[[:digit:]]+ { shorten(g_filename, atoi(strrchr(yytext, ':')+1)); }
++ @SHORT|$\* { shorten(g_filename, 1); }
++ @ARGS:[[:digit:]]+ { args(atoi(strrchr(yytext, ':')+1)); }
++ @ARGS|$\+ { args(-1); }
++ @LINE { FORMAT("%d", line); }
++ @\{ { ++expunge_depth; }
++ \} { if (!expunge_depth--) { ECHO; } }
++ \\\n { BEGIN PADDING; ++line; CHAR(' '); }
++ \\{MACROS} { STRING(yytext + 1); }
++ \n { CHAR('\n'); ++line; if (first_nl) { BEGIN STOP; first_nl = 0; tmpline = 0; } }
++ {SPACE} { BEGIN PADDING; CHAR(' '); }
++ . { ECHO; }
++}
++
++<PADDING>{
++ {SPACE} { ; }
++ .|\n { yyless(0); BEGIN FOUND; }
++}
++
++<STOP>{
++ @BAKE[[:space:]] { line += tmpline; goto bake; }
++ @STOP { BEGIN FOUND; yyless(0); }
++ \n { ++tmpline; yymore(); }
++ .|\\@ { yymore(); }
++}
++
++%%
++
++void root(char * filename) {
++ char * path, * terminator;
++ if (!(path = realpath(filename, NULL))) { return; }
++ if ((terminator = strrchr(path, '/'))) {
++ *terminator = '\0';
++ chroot(path);
++ }
++ free(path);
++}
++
++void args(int n) {
++ if (n < 0) { for (int i = 0; i < g_ac; ++i) { STRING(g_av[i]); if (i + 1 < g_ac) { CHAR(' '); } } }
++ else if (n < g_ac) { STRING(g_av[n]); }
++}
++
++void shorten(char * filename, int n) {
++ char * end = filename + strlen(filename);
++ while (n && (end = memrchr(filename, '.', end - filename))) { --n; }
++ if (!end) {
++ fprintf(stderr, "<SHORTEN> context error: Argument out of range.\n");
++ /* Ensures consistency. @SHORT will always return *something* that isn't filename */
++ STRING("idiot");
++ return;
++ }
++ FWRITE(filename, end - filename);
++}
++
++void help(void) { fputs("see bake(1) - \"Buy high. Sell low.\"\n", stderr); }
++
++int main (int ac, char ** av) {
++ int run = 1;
++ char * av0 = av[0];
++ FILE * fp;
++
++ /* supports long/short, -allinone, (-X ... -X=... -X<NUM>) */
++ while (++av, --ac) {
++ size_t i;
++ if (av[0][0] != '-') { goto start; }
++ if (av[0][1] == '-') {
++ if (av[0][2] == '\0') { ++av, --ac; goto start; }
++ if (!strcmp(av[0]+2, "dry-run")) { i = strlen(av[0]); goto opt_dry_run; }
++ if (!strcmp(av[0]+2, "select" )) { if (!ac-1 || isdigit(av[1][0])) { goto opt_arg; }
++ ++av, --ac; i = strlen(av[0]); goto opt_select; }
++ if (!strcmp(av[0]+2, "list" )) { i = strlen(av[0]); goto opt_list; }
++ if (!strcmp(av[0]+2, "help" )) { goto opt_help; }
++ goto opt_default;
++ }
++ for (i = 1; i < strlen(av[0]); ++i) {
++ switch (av[0][i]) {
++ opt_dry_run: case 'n': run = 0; break;
++ case 's':
++ /* Covers cases -<LAST>s<NUM> -<LAST>s <NUM> */
++ if (isdigit(av[0][i+1])) { g_select = atoi(av[0]+i+1); }
++ else if (ac > 1 && isdigit(av[1][0])) { ++av, --ac; opt_select: g_select = atoi(av[0]); }
++ else { g_select = 0; }
++ if (!g_select) { fprintf(stderr, "%s: Invalid argument for -s\n", av0); return 1; }
++ i = strlen(av[0]);
++ break;
++ opt_list: case 'l': run = 0; g_select = -1; break;
++ opt_help: case 'h': help(); return 0;
++ opt_default: default: fprintf(stderr, "%s: Unknown option '%s'\n", av0, av[0]); return 1;
++ opt_arg: fprintf(stderr, "%s: Argument missing for '%s'\n", av0, av[0]); return 1;
++ }
++ }
++ }
++
++ start:
++ if (!ac) { fprintf(stderr, "%s: Missing filename\n", av0); return 1; }
++ if (!g_select) { goto out_of_range; }
++
++ g_filename = av[0];
++ root(g_filename);
++ { /* ensures the filename doesn't have a relative path that would misdirect the command within the new root */
++ char * tmp = strrchr(g_filename, '/');
++ if (tmp) { g_filename = tmp+1; }
++ }
++
++ /* open and prepare ac, av */
++ if (!(yyin = fp = fopen(g_filename, "rb")))
++ { fprintf(stderr, "%s: '%s' %s\n", av0, g_filename, strerror(errno)); return 1; }
++ g_ac = --ac, g_av = ++av;
++
++ /* Prepares our UNIX pipe for input */
++ if (run) {
++ g_pipe = popen("/bin/sh -e", "w");
++ if (!g_pipe) { fprintf(stderr, "%s: <g_pipe> %s\n", av0, strerror(errno)); return 1; }
++ }
++
++ if (g_select > 0) { fprintf(stderr, "%s: ", av0); fflush(stderr); }
++ yylex(); fflush(stdout);
++ fclose(fp);
++ if (g_select > 0) { pclose(g_pipe); goto out_of_range; }
++
++ if (!run) { return 0; }
++ fprintf(stderr, "output: "); fflush(stderr);
++ run = pclose(g_pipe); /* repurposed run */
++ if (run) { printf("%s: Exit code %d\n", av0, run); }
++ return run;
++ out_of_range: fprintf(stderr, "%s: <%d> Out of range\n", av0, g_select); return 1;
++}
--- /dev/null
--- /dev/null
++#!/bin/bash
++
++# Originally written by Anon, modified by Emil to better match Bake functionality
++
++# Issues: sloooow, fails to handle multi-line statements
++
++VERSION="20240408"
++
++BLUE='\033[34m'
++GREEN='\033[32m'
++YELLOW='\033[93m'
++DIM='\033[2m'
++BOLD='\033[1m'
++NORMAL='\033[0m'
++
++MARKNAME="@BAKE"
++MARK="${MARKNAME} "
++MARKSTR="${GREEN}${MARKNAME}${NORMAL}"
++
++enable -n echo
++
++usage() {
++ echo -e "$0: [option] ${BOLD}target-file${NORMAL} [${GREEN}arguments${NORMAL} ...]\n"
++ echo -e "Use the format \`${BOLD}@BAKE${NORMAL} cmd ...' within the ${BOLD}target-file${NORMAL}."
++ echo -e "This will execute until the end of line, or if existing, until the ${BOLD}@STOP${NORMAL} marker.\n"
++ echo -e "Options [Must be first]"
++ echo -e "\t${DIM}-h --help${NORMAL}, ${BOLD}-n --dry-run${NORMAL}\n"
++ echo -e "Expansions\n"
++ echo -e "\t${YELLOW}@FILENAME${NORMAL} returns target-file (abc.x.txt)"
++ echo -e "\t${YELLOW}@SHORT${NORMAL} returns target-file without suffix (^-> abc.x)"
++ echo -e "\t${YELLOW}@ARGS${NORMAL} returns ${GREEN}arguments${NORMAL}"
++ echo -e "\t${YELLOW}@{${NORMAL}${BOLD}EXPUNGE_THIS_FILE${YELLOW}}${NORMAL} inline region to delete this or many files or directories,"
++ echo -e "\tnon-recursive, only one file per block, removed from left to right. This has no\n\tinfluence on the normal command execution.\n"
++}
++
++if [[ $# -lt 1 ]]; then
++ usage
++ exit 1
++fi
++
++if [[ $1 == "-h" ]] || [[ $1 == "--help" ]]; then
++ usage
++ exit 0
++fi
++
++if [[ $1 == "-v" ]] || [[ $1 == "--version" ]]; then
++ echo -e "$0: $VERSION"
++ exit 0
++fi
++
++run=1
++if [[ $1 == "-n" ]] || [[ $1 == "--dry-run" ]]; then
++ if [[ $# -lt 2 ]]; then
++ usage
++ exit 1
++ fi
++ run=0
++ shift 1
++fi
++
++input_file=$1
++shift 1
++
++if [[ ! -f $input_file ]]; then
++ echo -e "Input file '$input_file' does not exist." >&2
++ exit 1
++fi
++
++cd $(dirname "$(readlink -f "$input_file")")
++input_file=${input_file##*/}
++
++line=$(grep "$MARK" "$input_file" | head -1)
++
++if [[ -n $line ]]; then
++
++ line=${line//\$@/$input_file}
++ line=${line//\$\*/${input_file%.*}}
++ line=${line//\$+/$@}
++ line=${line//@FILENAME/$input_file}
++ line=${line//@SHORT/${input_file%.*}}
++ line=${line//@ARGS/$@}
++ line=${line//@NAME/$input_file}
++ line=${line//@FILE/$input_file}
++ line=$(echo "$line" | sed 's/@STOP.*//')
++
++ echo -e "${BOLD}${GREEN}$0${NORMAL}: ${line#*${MARK}}"
++
++ line=$(echo "$line" | sed -E 's/@\{([^ \}]+?)\}/\1/')
++ command="${line#*${MARK}}"
++ if [[ $run -eq 1 ]]; then
++ echo -e "${BOLD}${GREEN}output${NORMAL}:"
++ sh -c "$command"
++ fi
++else
++ echo -e "${MARKSTR} is not defined." >&2
++ exit 1
++fi