Shell Scripting with MiniScript

JoeStrout - Jun 27 - - Dev Community

You can use command-line MiniScript 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). 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And now, you can execute that script by doing just

./hello
Enter fullscreen mode Exit fullscreen mode

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) 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
Enter fullscreen mode Exit fullscreen mode

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 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
Enter fullscreen mode Exit fullscreen mode

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 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
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .