Create your own command shell for Mini Micro

joestrout

JoeStrout

Posted on January 3, 2024

Create your own command shell for Mini Micro

Mini Micro takes a simplistic and unified approach to the command line: the command line is just a MiniScript REPL. There are global functions available for your typical shell commands, such cd (change directory), pwd (print working directory), and run. But, because this is a MiniScript REPL, you're typing in MiniScript syntax. That means that all string literals (such as file or path names) have to be quoted. And you certainly can't type the name of a file to run it.

All this is fine once you get used to it, but often causes a stumbling block for new users. And even experienced users may miss some of the niceties of their favorite command shell.

Fortunately, Mini Micro is also made to be highly customizable! And that even gives you the tools to create your own command shell, exactly as you like it.

Introducing mash

Here we are going to build a little command shell called mash (a portmanteau of "Mini Bash"). Begin by launching Mini Micro, and using the edit command to start editing a new program. Then type or paste the following:

// Mini Bash, or "mash"
import "stringUtil"

mashPath = "/usr/mash"

mash = function
    env.shell = mashPath
    exit
end function

_savedGlobals.mash = @mash

splitQuoted = function(s, delim=" ", quoteChar="""")
    a = s.split(quoteChar)
    b = []
    for n in a.indexes
        if n % 2 == 0 then
            for m in a[n].split(delim)
                if m then b.push m
            end for
        else
            b.push a[n]
        end if
    end for
    return b
end function
Enter fullscreen mode Exit fullscreen mode

This gets the ball rolling by defining where this mash program is going to live, making a function to re-invoke it as the shell (by assigning the right path to env.shell, and then exiting MiniScript to return to that shell), and making sure this mash command survives a reset by stuffing it into _savedGlobals. Then, because the current version of /sys/lib/stringUtil does not include a splitQuoted function, we define a simple one here.

Save this program as /usr/mash.ms, then continue editing. We're going to add a commands map, with one entry (or sometimes two) for each command we want to support.

commands = {}

commands.cd = function(args)
    if args.len < 2 then
        cd
    else if args.len > 2 then
        print "cd: Too many arguments."
        _printMark "Usage: `cd path`"
    else
        cd args[1]
    end if
    commands.pwd
end function

commands.clear = function(args)
    clear
    text.color = color.orange
end function
commands.cls = @commands.clear  // make "cls" an alias for "clear"

commands.exit = function(args)
    _printMark "Enter `mash` to return to the mash shell."
    env.shell = ""
    exit
end function

commands.ls = function(args)
    if args.len == 2 then
        olddir = file.curdir
        cd args[1]
        files = file.children
        cd olddir
    else
        files = file.children
    end if
    files.sort
    for name in files
        if text.column > 51 then
            print
        else
            text.column = ceil(text.column/17)*17
        end if
        print name, ""
    end for
    if text.column > 0 then print
end function
commands.dir = @commands.ls  // make "dir" an alias for "ls"

commands.pwd = function(args)
    print file.curdir
end function
Enter fullscreen mode Exit fullscreen mode

The commands our miniature shell supports are cd, clear, exit, ls, and pwd, with a couple of aliases (cls for clear, and dir for ls). I hacked out these methods rather quickly; feel free to build on them!

Next, a nice thing about most shells is that you can just type the name of a program (sometimes preceded by "./") at the shell prompt to run it. So let's make a program that does this — and goes a little further; if the filename you enter is not a MiniScript program, but something like a text file, image, or sound, then we'll view it with the view command.

runProgram = function(args)
    info = file.info(args[0])
    if info == null then info = file.info(args[0] + ".ms")
    if info == null then return false
    if info.isDirectory then
        print args[0] + ": is directory"
    else if info.path.endsWith(".ms") then
        env.shell = mashPath
        load info.path
        run
    else
        view info.path
    end if
    return true
end function
Enter fullscreen mode Exit fullscreen mode

Note that one thing mash does not do in the above runProgram function is search a standard search path. That would be a nice feature — feel free to add it!

Finally, we need a main program to get input from the user, and figure out what to do with it.

// main loop
while true
    if text.column > 0 then print
    cmdLine = input("mash>")
    args = splitQuoted(cmdLine)
    if not args or not args[0] then continue
    if not commands.hasIndex(args[0]) then
        if not runProgram(args) then
            print "Unknown command: " + args[0]
        end if
        continue
    end if
    f = commands[args[0]]
    f args
end while
Enter fullscreen mode Exit fullscreen mode

Boiled down to its essence, this main loop just does this: get input from the user, split it on spaces, and see if the first word is some function in our commands map. If so, run that function; otherwise try to run a program with a matching name.

Benefits

Screen shot of  raw `mash` endraw  in action

While simple, this program already provides many of the benefits of a standard bash-like command shell:

  • No need to put quotation marks around file names and paths!
  • No need to type run to run a program.
  • Fully customizable directory listings and other behavior (for example, mash's clear command also resets the text color).

Of course there's always a trade-off; you lose the ability to type MiniScript code at any command prompt you see. If you're at the "mash>" prompt, you'll need to exit to get to the MiniScript REPL, and then enter mash to return to your shell.

Taking it Further

Our miniature shell is only about 100 lines at this point. There's plenty of room for expansion! Here are some ideas to get you started:

  • Add an edit command that just calls through to the standard one.
  • Make the ls command accept arguments, like "-l" for long form.
  • Also make the ls command color the file names according to their file type.
  • Add a search path, so when you write a little MiniScript program that's generally useful, you can invoke it by name no matter what directory you're in.
  • Add support for glob patterns in any file name or path.
  • Add more file commands such as mkdir, rm, find, etc.

If you decide to live with a shell rather than just the MiniScript REPL, I'm sure you'll find things you'd like to add or change. And now you can do that. The power is yours!

💖 💪 🙅 🚩
joestrout
JoeStrout

Posted on January 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related