Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a primitive version of the Argparse module #131

Merged
merged 12 commits into from
May 1, 2024
4 changes: 2 additions & 2 deletions core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Github: @Almas-Ali
"""

from core.builtin_funcs import run
from core.builtin_funcs import run, global_symbol_table, radonify

__all__ = ["run"]
__all__ = ["run", "global_symbol_table", "radonify"]
__version__ = "0.0.1"
11 changes: 0 additions & 11 deletions core/builtin_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,16 +352,6 @@ def execute_pyapi(self, exec_ctx):
return res
return res.success(Null.null())

@args([])
def execute_sys_args(self, exec_ctx):
from sys import argv # Lazy import

try:
return RTResult().success(Array(argv))
except Exception as e:
print(e)
return RTResult().failure(RTError(self.pos_start, self.pos_end, "Could't run the sys_args", exec_ctx))

@args([])
def execute_time_now(self, exec_ctx):
import time # Lazy import
Expand Down Expand Up @@ -522,7 +512,6 @@ def create_global_symbol_table() -> SymbolTable:
# System methods
ret.set("require", BuiltInFunction("require"))
ret.set("exit", BuiltInFunction("exit"))
ret.set("sys_args", BuiltInFunction("sys_args"))
ret.set("time_now", BuiltInFunction("time_now"))
# Built-in classes
ret.set("File", bic.BuiltInClass("File", bic.FileObject))
Expand Down
5 changes: 5 additions & 0 deletions core/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@ def set_index(self, index: Value, value: Value) -> ResultTuple:
)
return self, None

def contains(self, other: Value) -> ResultTuple:
if not isinstance(other, String):
return None, self.illegal_operation(other)
return Boolean(other.value in self.value), None

def is_true(self) -> bool:
return len(self.value) > 0

Expand Down
2 changes: 1 addition & 1 deletion core/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def make_tokens(self) -> tuple[list[Token], Optional[Error]]:
self.advance()
elif self.current_char == "!":
token, error = self.make_not_equals()
if error is None:
if error is not None:
return [], error
assert isinstance(token, Token)
tokens.append(token)
Expand Down
1 change: 0 additions & 1 deletion core/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,6 @@ def array_expr(self) -> ParseResult[Node]:

if self.current_tok.type == TT_RSQUARE:
self.advance(res)
self.skip_newlines()
else:
elt = res.register(self.expr())
self.skip_newlines()
Expand Down
4 changes: 2 additions & 2 deletions core/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ def __init__(
self.type = type_
self.value = value

self.pos_start = pos_start
self.pos_end = pos_end if pos_end is not None else pos_start
self.pos_start = pos_start.copy()
self.pos_end = pos_end.copy() if pos_end is not None else pos_start.copy()

def matches(self, type_: TokenType, value: TokenValue) -> bool:
return self.type == type_ and self.value == value
Expand Down
23 changes: 5 additions & 18 deletions examples/args_test.rn
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import Argparser

parser = Argparser("Utility Tool", "A simple utility tool.")
parser = Argparser.Argparser()
parser.add_flag("--version", "-v", "Show version")
parser.add_named("--output", "Output file")
parser.add_pos_opt("filename", "File")
print(parser.parse(argv))
Almas-Ali marked this conversation as resolved.
Show resolved Hide resolved

# Callback function
fun version() -> "1.0.0"
fun author() -> "Md. Almas Ali"

# Add commands
parser.add_command("-v", "Version information", 0, version)
parser.add_command("-a", "About author", 0, author)

# Get args lists
# print(parser.get_args())

# Get help text
# print(parser.get_help())

# print(parser.get_commands())

parser.parse()
71 changes: 49 additions & 22 deletions radon.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/usr/bin/python3.12
# By: Md. Almas Ali

import argparse
import pathlib
import sys
from typing import IO

try:
import readline
Expand All @@ -17,6 +16,8 @@
pass

import core as base_core
from core.parser import Context
from core.lexer import Position


def shell() -> None:
Expand Down Expand Up @@ -57,22 +58,51 @@ def shell() -> None:
print("KeyboardInterrupt")


def main(argv: list[str]) -> None:
parser = argparse.ArgumentParser(description="Radon programming language")
parser.add_argument(
"-p",
"--hide-file-paths",
help="Don't show file paths in error messages [NOT CURRENTLY WORKING]",
action="store_true",
def usage(program_name: str, stream: IO[str]) -> None:
print(
f"Usage: {program_name} [--source | -s] [--command | -c] [source_file] [--version | -v] [--help | -h]",
file=stream,
)
parser.add_argument("-s", "--source", type=str, help="Radon source file", nargs="*")
parser.add_argument("-c", "--command", type=str, help="Command to execute as string")
parser.add_argument("-v", "--version", help="Version info", action="store_true")
args = parser.parse_args()

if args.source:
source = pathlib.Path(args.source[0]).read_text()
(result, error, should_exit) = base_core.run(args.source[0], source, hide_paths=args.hide_file_paths)

def main(argv: list[str]) -> None:
program_name = argv.pop(0)
source_file = None
command = None
while len(argv) > 0:
arg = argv.pop(0)
match arg:
case "--help" | "-h":
usage(program_name, sys.stdout)
exit(0)
case "--source" | "-s":
if len(argv) == 0:
usage(program_name, sys.stderr)
print(f"ERROR: {arg} requires an argument", file=sys.stderr)
exit(1)
source_file = argv[0]
break # allow program to use remaining args
case "--version" | "-v":
print(base_core.__version__)
exit(0)
case "--command" | "-c":
if len(argv) == 0:
usage(program_name, sys.stderr)
print(f"ERROR: {arg} requires an argument", file=sys.stderr)
exit(1)
command = argv[0]
break # allow program to use remaining args
case _:
usage(program_name, sys.stderr)
print(f"ERROR: Unknown argument '{arg}'", file=sys.stderr)
exit(1)

pos = Position(0, 0, 0, "<argv>", "<argv>")
base_core.global_symbol_table.set("argv", base_core.radonify(argv, pos, pos, Context("<global>")))
if source_file is not None:
with open(source_file, "r") as f:
source = f.read()
(result, error, should_exit) = base_core.run(source_file, source)

if error:
print(error.as_string())
Expand All @@ -81,18 +111,15 @@ def main(argv: list[str]) -> None:
if should_exit:
exit()

elif args.command:
(result, error, should_exit) = base_core.run("<stdin>", args.command)
elif command is not None:
(result, error, should_exit) = base_core.run("<cli>", command)

if error:
print(error.as_string())

elif args.version:
print(base_core.__version__)

else:
shell()


if __name__ == "__main__":
main(sys.argv[1:])
main(sys.argv)
171 changes: 125 additions & 46 deletions stdlib/Argparser.rn
Original file line number Diff line number Diff line change
@@ -1,62 +1,141 @@
class Argparser {
fun __constructor__(description) {
this.description = description
this.options = {
"--help": "Show this help message and exit"

_i = 0
FULLNAME_INVALID = _i++
FULLNAME_FLAG = _i++
FULLNAME_NAMED = _i++
_i = 0

fun str_starts_with(s, prefix) {
if str_len(s) < str_len(prefix) {
return false
}
for i = 0 to str_len(prefix) {
if s[i] != prefix[i] {
return false
}
}
return true
}

fun add_option(option, description, is_flag = false) {
this.options[option] = {"description": description, "is_flag": is_flag}
class Argparser {
fun __constructor__(desc="") {
this.desc = desc
this.pos_opts = []
this.flags = []
this.named = []
}

fun parse_arguments() {
arguments = sys_args()
parsed_args = {}
current_option = null
index = 1
fun add_pos_opt(name, desc) {
arr_append(this.pos_opts, {"name": name, "desc": desc})
return this
}

while index < arr_len(arguments) {
argument = arguments[index]
fun add_flag(fullname, shortname, desc) {
assert str_starts_with(fullname, "--"), "Flags must start with '--'"
assert str_starts_with(shortname, "-"), "Flag shortnames must start with '-'"
assert not str_starts_with(shortname, "--"), "Flag shortnames must not start with '--'"
arr_append(this.flags, {"fullname": fullname, "shortname": shortname[1:], "desc": desc})
return this
}

if current_option != null {
parsed_args[current_option] = argument
current_option = null
}
elif argument in this.options {
current_option = argument
}
else {
print("Unknown argument: " + argument)
return
}
fun add_named(name, desc) {
assert str_starts_with(name, "--"), "Named arguments must start with '--'"
arr_append(this.named, {"name": name, "desc": desc})
return this
}

nonlocal index++
fun usage(program_name) {
ret = "Usage: "+program_name+" <flags> <options>\n"
ret += "OPTIONS:\n"
for opt in this.pos_opts {
nonlocal ret += " " + opt["name"] + ": " + opt["desc"] + "\n"
}
ret += "FLAGS:\n"
for flag in this.flags {
nonlocal ret += " " + flag["fullname"] + ", " + flag["shortname"] + ": " + flag["desc"] + "\n"
}
for named in this.named {
nonlocal ret += " " + named["name"] + " <value>: " + named["desc"] + "\n"
}
return ret
}

return parsed_args
fun report_error(program_name, msg) {
print(this.usage(program_name))
print("ERROR: "+msg)
pyapi("exit(1)", {})
}

fun print_help() {
print("Usage:" + sys_args()[0])
print(this.description)
print("Options:")
for option, data in this.options {
print(" " + option + ": " + data["description"])
fun flag_by_shortname(shortname) {
for flag in this.flags {
if flag["shortname"] == shortname {
return flag
}
}
return null
}

fun fullname_type(fullname) {
for flag in this.flags {
if flag["fullname"] == fullname {
return FULLNAME_FLAG
}
}
for named in this.named {
if named["name"] == fullname {
return FULLNAME_NAMED
}
}
return FULLNAME_INVALID
}
}

#!
argparser = Argparser("This is a simple argparser")
argparser.add_option("--name", "Name of the user")
argparser.add_option("--age", "Age of the user")
argparser.add_option("--is-admin", "Is the user an admin", is_flag=true)

args = argparser.parse_arguments()
if "--help" in args {
argparser.print_help()
} else {
print(args)
fun parse(args) {
pos_opts = this.pos_opts
flags = this.flags

program_name = arr_pop(args, 0)
parsed = {}
for pos_opt in pos_opts {
parsed[pos_opt["name"]] = null
}
for flag in flags {
parsed[flag["fullname"]] = false
}
for named in this.named {
parsed[named["name"]] = null
}
pos_opts_idx = 0
while arr_len(args) > 0 {
arg = arr_pop(args, 0)
if str_starts_with(arg, "-") {
if str_starts_with(arg, "--") {
switch this.fullname_type(arg) {
case FULLNAME_INVALID -> this.report_error(program_name, "unknown flag: '"+arg+"'")
case FULLNAME_FLAG -> parsed[arg] = true
case FULLNAME_NAMED {
value = arr_pop(args, 0)
parsed[arg] = value
}
}
} else {
for letter in arg[1:] {
flag = this.flag_by_shortname(letter)
if is_null(flag) {
this.report_error(program_name, "unknown flag: '-"+letter+"'")
} else {
parsed[flag["fullname"]] = true
}
}
}
} else {
if pos_opts_idx >= arr_len(pos_opts) {
this.report_error(program_name, "unexpected positional argument: '" + arg + "'")
}
arg_name = pos_opts[pos_opts_idx]["name"]
parsed[arg_name] = arg
nonlocal pos_opts_idx++
}
}
return parsed
}
}
!#
Loading