fundamentals.md 14 KB

Work in progress.

This tutorial introduces the fundamentals of shell programming with Elvish. Familiarity with other shells or programming languages is useful but not required.

Commands and strings

Let's begin with the traditional "hello world" program:

~> echo Hello, world!
Hello, world!

Here, we call the echo command with two arguments: Hello, and world!. The echo command prints them both, inserting a space in between and adding a newline at the end, and voilà, we get back the message Hello, world!.

Quoting the argument

We used a single space between the two arguments. Using more also works:

~> echo Hello,  world!
Hello, world!

The output still only has one space, because echo didn't know how many spaces were used to separate the two arguments; all it sees is the two arguments, Hello, and world!. However, you can preserve the two spaces by quoting the entire text:

~> echo "Hello,  world!"
Hello,  world!

In this version, echo only sees one argument containing Hello,  world (with two spaces). A pair of double quotes tells Elvish that the text inside it is a single argument; the quotes themselves are not part of the argument.

On contrary, the Hello, and world! arguments are implicitly delimited by spaces (instead of explicitly by quotes); as such, they are known as barewords. Some special characters also delimit barewords, which we will see later. Barewords are useful to write command names, filenames and command-line switches, which usually do not contain spaces or special characters.

Editing the command line

TODO

Builtin and external commands

We demonstrated the basic command structure using echo, a very simple command. The same structure applies to all the commands. For instance, Elvish comes with a randint command that takes two arguments a and b and generates a random integer in the range a...b-1. You can use the command as a digital dice:

~> randint 1 7
▶ 3

Arithmetic operations are also commands. Like other commands, the command names comes first, making the syntax a bit different from common mathematical notations:

~> + 17 28 # addition
▶ (num 45)
~> * 17 28 # multiplication
▶ (num 476)

The commands above all come with Elvish; they are builtin commands. There are many more of them.

Some builtin commands are in importabled modules. For example, the command to compute exponention lives in the math: module:

~> use math
~> math:pow 2 10
▶ (num 1024)

Another kind of commands is external commands. They are separate programs from Elvish, and either come with the operating system or are installed by you manually. Chances are you have already used some of them, like ls for listing files, or cat for showing files.

There are really a myriad of external commands; to start with, you can manage code repositories with git, convert documents with pandoc, process images with ImageImagick, transcode videos with ffmpeg, test the security of websites with nmap and analyze network traffic with tcpdump. Many free and open-source software come with a command-line interface.

Here we show you how to obtain the latest version of Elvish entirely from the command line: we use curl to download the binary and its checksum, shasum to check the checksum, and chmod to make it executable (assuming that you are running macOS on x86-64):

~> curl -s -o elvish https://dl.elv.sh/darwin-amd64/elvish-HEAD
~> curl -s https://dl.elv.sh/darwin-amd64/elvish-HEAD.sha256sum
8b3db8cf5a614d24bf3f2ecf907af6618c6f4e57b1752e5f0e2cf4ec02bface0  elvish-HEAD
~> shasum -a 256 elvish
8b3db8cf5a614d24bf3f2ecf907af6618c6f4e57b1752e5f0e2cf4ec02bface0  elvish
~> chmod +x elvish
~> ./elvish

History and scripting

Some commands are useful to rerun; for instance, you may want to roll the digital dice several times in a roll, or for another occasion. Of course, you can just retype the command:

~> randint 1 7
▶ 1

The command is short, but still, it can become a chore if you want to run it repeatedly. Fortunately, Elvish remembers all the commands you have typed; you can just ask Elvish to recall it by pressing Up:

@ttyshot learn/fundamentals/history-1

This will give you the last command you have run. However, it may have been a while when you have last run the randint command, and this will not give you what you need. You can either continue pressing Up until you find the command, or you can give Elvish a hint by typing some characters from the command line you want, e.g. ra, before pressing Up:

@ttyshot learn/fundamentals/history-2

Another way to rerun commands is saving them in a script, which is simply a text file containing the commands you want to run. Using your favorite text editor, save the command to dice.elv under your home directory:

# dice.elv
randint 1 7

After saving the script, you can run it with:

~> elvish dice.elv
▶ 4

Since the above command runs elvish explicitly, it works in other shells as well, not just from Elvish itself.

Variables and lists

To change what a command does, we now need to change the commands themselves. For instance, instead of saying "Hello, world!", we might want our command to say "Hello, John!":

~> echo Hello, John!
Hello, John!

Which works until you want a different message. One way to solve this is using variables:

~> var name = John
~> echo Hello, $name!
Hello, John!

The command echo Hello, $name! uses the $name variable you just assigned in the previous command. To greet a different person, you can just change the value of the variable, and the command doesn't need to change:

~> var name = Jane
~> echo Hello, $name!
Hello, Jane!

Using variables has another advantage: after defining a variable, you can use it as many times as you want:

~> var name = Jane
~> echo Hello, $name!
Hello, Jane!
~> echo Bye, $name!
Bye, Jane!

Now, if you change the value of $name, the output of both commands will change.

Environment variables

In the examples above, we have assigned value of $name ourselves. We can also make the $name variable automatically take the name of the current user, which is usually kept in an environment variable called USER. In Elvish, environment variables are used like other variables, except that they have an E: at the front of the name:

~> echo Hello, $E:USER!
Hello, elf!
~> echo Bye, $E:USER!
Bye, elf!

The outputs will likely differ on your machine.

Lists and indexing

The values we have stored in variables so far are all strings. It is possible to store a list of values in one variable; a list can be written by surrounding some values with [ and ]. For example:

~> var list = [linux bsd macos windows]
~> echo $list
[linux bsd macos windows]

Each element of this list has an index, starting from 0. In the list above, the index of linux is 0, that of bsd is 1, and so on. We can retrieve an element by writing its index after the list, also surrounded by [ and ]:

~> echo $list[0] is at index 0
linux is at index 0

We can even do:

~> echo [linux bsd macos windows][0] is at index 0
linux is at index 0

Note that in this example, the two pairs of [] have different meanings: the first pair denotes lists, while the second pair denotes an indexing operation.

Script arguments

Recall the dice.elv script above:

# dice.elv
randint 1 7

And how we ran it:

~> elvish dice.elv
▶ 4

We were using elvish itself as a command, with the sole argument dice.elv. We can also supply additional arguments:

~> elvish dice.elv a b c
▶ 4

But this hasn't made any difference, because well, our dice.elv script doesn't make use of the arguments.

The arguments are kept in a $args variable, as a list. Let's try put this into a show-args.elv file in your home directory:

echo $args

And we can run it:

~> elvish show-args.elv
[]
~> elvish show-args.elv foo
[foo]
~> elvish show-args.elv foo bar
[foo bar]

Since $args is a list, we can retrieve the individual elements with $args[0], $args[1], etc.. Let's rewrite our greet-and-bye script, taking the name as an argument. Put this in greet-and-bye.elv:

var name = $args[0]
echo Hello, $name!
echo Bye, $name!

We can run it like this:

~> elvish greet-and-bye.elv Jane
Hello, Jane!
Bye, Jane!
~> elvish greet-and-bye.elv John
Hello, John!
Bye, John!

Output capture and multiple values

Environment variables are not the only way to learn about a computer system; we can also gain more information by invoking commands. The uname command tells you which operation system the computer is running; for instance, if you are running Linux, it prints Linux (unsurprisingly):

~> uname
Linux

(If you are running macOS, uname will print Darwin, the open-source core of macOS.)

Let's try to integrate this information into our "hello" message. The Elvish command-line allows us to run multiple commands in a batch, as long as they are separated by semicolons. We can build the message by running multiple commands, using uname for the OS part:

~> echo Hello, $E:USER, ; uname ; echo user!
Hello, xiaq,
Linux
user!

This has the undesirable effect that "Linux" appears on its own line. Instead of running this command directly, we can first capture its output in a variable:

~> var os = (uname)
~> echo Hello, $E:USER, $os user!
Hello, elf, Linux user!

You can also use the output capture construct directly as an argument to echo, without storing the result in a variable first:

~> echo Hello, $E:USER, (uname) user!
Hello, elf, Linux user!

More arithmetic

You can use output captures to construct do complex arithmetic involving more than one operation:

~> # compute the answer to life, universe and everything
   * (+ 3 4) (- 100 94)
▶ 42