Shell Scripting with MiniScript
Tags: miniscript, commandline, shell, shellscripts
Published: 2024-06-27 17:06:21

You can use [command-line MiniScript](https://miniscript.org/cmdline/) to write shell scripts that can be invoked directly, just like scripts written in BASH, Python, Perl, etc.  If you're already comfortable with MiniScript, this lets you use those skills to write scripts that manipulate files, invoke other shell commands, and otherwise improve your command-line life.

## Using a shebang

Several features of command-line MiniScript support this sort of usage.  First, when executing a script file, it ignores the first line if it starts with "#!" (a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix))).  A shebang is used to direct the shell to the proper interpreter for the script.  So, you can make a text file like this:

```
#!/usr/local/bin/miniscript
print "Hello world!"
print "The current directory is: " + file.curdir
print "And my arguments are: " + shellArgs
```

Change the path after the shebang in the first line to point to wherever your command-line MiniScript lives (`which miniscript` will tell you that if you've forgotten).  Save the file as, for example, "hello" (traditionally, you don't use any file extension for an executable shell script).  If your editor doesn't automatically detect the shebang and set the executable bit, you can set it with a command like:

```
chmod u+x hello
```
And now, you can execute that script by doing just
```
./hello
```
Or if you drop the script somewhere in your PATH, then you don't even need `./`; you can just type `hello`.  Neat, huh?

## The `file` module

Shell scripts are very often written to do some task in your file system.  For this, the intrinsic `file` module will be helpful.  This contains a bunch of commands (documented [here](https://miniscript.org/cmdline/)) for getting and changing the current directory, getting info on files, copying/renaming/deleting files, and reading/writing text files.

We'll see a practical example of this in the next section, so let's get to it!

## `shellArgs`

The next useful feature to know about is `shellArgs`.  This gives you all the arguments to your shell script, as a list of strings.  `shellArgs[0]` is always the path to the script file itself, which can be useful if you need some data file or import module that's stored next to it.  The remaining entries in the list are the arguments, which could be command-line options or file paths — use these for whatever you like.  Note that if you give a wildcard argument, like `*.txt`, the shell will automatically expand this into all matching file names, before invoking your script.

As an example, let's make a "purge" command that deletes any files it's given _if_ they are more than 30 days old.

```
#!/usr/local/bin/miniscript
import "dateTime"
dayLimit = 30
limit = dayLimit * 24 * 60 * 60  // time limit, in seconds
oldFiles = []
for f in shellArgs[1:]
	info = file.info(f)
	if info.isDirectory then
		print f + ": directory"
		continue
	end if
	secsOld = dateTime.nowVal - dateTime.val(info.date)
	if secsOld > limit then oldFiles.push f
end for
if not oldFiles then
	print "No given files are over " + dayLimit + " days old."
	exit
end if
print "The following files are over " + dayLimit + " days old:"
for f in oldFiles
	print "   " + f + " (" + file.info(f).date + ")"
end for
yesNo = input("Delete these files? ").lower
if yesNo and yesNo[0] == "y" then
	count = 0
	for f in oldFiles
		if file.delete(f) then count += 1
	end for
	print count + " file(s) deleted."
end if
```
Here we're using `shellArgs[1:]` to get the list of files passed to the script, and then `file.info` to get some details about each one.  This script skips directories (of course it could be expanded to handle those too), and then collects all the other files more than 30 days old, with the help of the [dateTime](https://github.com/JoeStrout/minimicro-sysdisk/blob/master/sys/lib/dateTime.ms) module.

This script also demonstrates the use of `input` to get more information from the user — in this case, to confirm deletion of the old files that were found.

## `env`

Shell scripts often find it useful to interact with the shell environment variables.  In command-line MiniScript, these are accessed via the `env` map.  If you do

```
print env.indexes
```

then you will see the names of all environment variables.  You can get a value using square-brackets syntax, like `env["PATH"]`, or with simply dot syntax, like `env.PATH`.

Modifications to `env` change the environment variables, but only for the script session.  So this is a good way to set up an environment for some other shell command you might execute with:

## `exec`

The `exec` intrinsic spawns a shell command.  It can run any non-interactive command that you could run yourself on the command line: manipulate docker images, convert images from one format to another, repack video files, zip or unzip files, push the latest version of your game up to itch.io, etc.

`exec` returns a little map with three entries:

- `status`: the exit code of the command, which is generally 0 for success, and some nonzero number for failure
- `output`: whatever the command printed to _stdout_
- `errors`: whatever the command printed to _stderr_

(See [Standard Streams](https://en.wikipedia.org/wiki/Standard_streams) for more insight on _stdout_ and _stderr_.)

As an example, here's a script for MacOS that checks whether any of the given files have been quarantined by the OS, and if so, clears the quarantine flag so they can be used.

```
#!/usr/local/bin/miniscript
import "stringUtil"
for f in shellArgs[1:]
	attrs = exec("xattr -l " + f).output
	if attrs.contains("com.apple.quarantine") then
		exec "xattr -d com.apple.quarantine " + f
		print "Cleared quarantine on " + f
	else
		print f + " is not in quarantine."
	end if
end for
```

## Conclusion

With these tools — the shebang, plus `file`, `shellArgs`, `env`, and `exec` — you have an extremely powerful tool in your toolbox.  Give yourself elegant commands that suit your needs, or even schedule them to run automatically, all while working in the clean, modern language of MiniScript.

How might you use this power?  Post your ideas or experiences in the comments below!