Create your own command shell for Mini Micro
Tags: miniscript, minimicro, programming, shell
Published: 2024-01-03 15:59:10

[Mini Micro](https://miniscript.org/MiniMicro/) 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](https://dev.to/joestrout/customizing-the-prompt-in-mini-micro-1l9j)!  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
```

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 `exit`ing 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
```

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
```

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
```

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 `mash` in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yxsonqzve106wb5l5lhy.png)

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](https://en.wikipedia.org/wiki/Glob_(programming)) 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!