# Your first Ruby script

> Write a script in rsh, Ruby with terminal commands in it: a file ending in .rsh, Ruby and commands on the same page, why each value stays one argument, catching a command that failed, and running it with ./ and with wol script.

Source: https://wolos.org/manual/first-ruby-script

## What it is

rsh is Ruby with terminal commands in it. A script in rsh is a Ruby program, and any line that starts with a word Ruby does not know is run as a command, exactly as you would type it in the terminal. So one file holds both: Ruby to count, decide and repeat, and commands such as `ls`, `mkdir` and `cp` to do the work.

rsh comes with WolOS. Its scripts end in `.rsh`. If you have read [Your first shell script](/manual/first-shell-script), this page climbs the same ladder: a file, the first line, making it run, running it, then values, a loop and what to do when something fails.

## Why WolOS does it this way

A shell script is quick to write and easy to get wrong. Every value is text that the shell splits again at each space, so a file called *Summer Trip.jpg* turns into two files that do not exist unless you remember the quote marks every time. A command that fails is easy to miss, and the script carries on.

Ruby has none of those traps, but in plain Ruby running a command takes a whole line of code. rsh keeps both: you write commands the way you type them, and they get Ruby's care. A value you put into a command is always exactly one argument, whatever it holds. A command that fails stops the script and says where, unless you catch it and decide what to do.

## How

### 1. Make a file

Open **Text Editor** (press Super+Space and type "text"), and type:

```ruby
#!/usr/bin/env rsh
# My first rsh script: Ruby and commands together.
name = ENV.fetch("USER")
puts "Hello, #{name}!"
ls ~/Pictures/Holiday
```

Save it in your home folder as `hello.rsh`.

What each line does:

| Line | Means |
|---|---|
| `#!/usr/bin/env rsh` | The first line, the shebang: "read this file with rsh". |
| `# My first rsh …` | A comment, for people. |
| `name = ENV.fetch("USER")` | Ruby: puts your account name in the variable `name`. |
| `puts "Hello, #{name}!"` | Ruby: prints a line. `#{…}` puts a value into the text. |
| `ls ~/Pictures/Holiday` | A command: Ruby has nothing called `ls`, so rsh runs it as in the terminal. |

How rsh tells them apart is the first word of the line. Something Ruby knows (`name`, `puts`, `if`, `end`, a name starting with a capital) makes the line Ruby. A word Ruby does not know makes it a command.

### 2. Make it run and run it

Open a terminal (Super+Return) and type, once:

```
chmod +x hello.rsh
```

Then run it:

```
./hello.rsh
```

You see:

```
Hello, aliceliddell!
Summer Trip.jpg
beach.jpg
cake.JPG
```

The first line came from Ruby, the rest from `ls`.

WolOS's own command runs it too, with no `chmod` needed, and can read it for mistakes first without running anything:

```
wol script check hello.rsh
wol script run hello.rsh
```

The check answers `hello.rsh: rsh script, parses and rsh is clean`: every line made sense, and rsh found nothing risky in it.

### 3. Ruby uses what a command says

Put a command between back-quotes, `` ` ``, and Ruby gets its answer as text. Save this as `count.rsh`:

```ruby
#!/usr/bin/env rsh
# Counts the photos in a folder of Pictures.
# How: ./count.rsh Holiday
folder = ARGV.fetch(0)
photos = `ls ~/Pictures/#{folder}`.lines.size
puts "#{folder} holds #{photos} photos."
```

`ARGV.fetch(0)` is the first argument you give the script. `.lines.size` counts the lines of what `ls` printed. Run `chmod +x count.rsh`, then `./count.rsh Holiday`, and you see `Holiday holds 3 photos.`

### 4. A loop, with one argument per value

This script moves the photos of a folder into a new folder, Sorted, and writes the ending `.JPG` in small letters on the way. Save it as `sort.rsh`:

```ruby
#!/usr/bin/env rsh
# Moves the photos of a folder into Sorted, with .jpg in small letters.
# How: ./sort.rsh Holiday
folder = File.join(ENV.fetch("HOME"), "Pictures", ARGV.fetch(0))
mkdir -p #{folder}/Sorted
count = 0
Dir.glob("#{folder}/*.{jpg,JPG}").each do |photo|
  name = File.basename(photo).sub(/\.JPG\z/, ".jpg")
  mv #{photo} #{folder}/Sorted/#{name}
  count += 1
end
puts "Moved #{count} photos into Sorted."
```

Run `chmod +x sort.rsh`, then `./sort.rsh Holiday`. You see `Moved 3 photos into Sorted.`, and in Files, Pictures/Holiday/Sorted holds *Summer Trip.jpg*, *beach.jpg* and *cake.jpg*.

| Lines | Mean |
|---|---|
| `folder = File.join(…)` | Ruby builds the path of the folder from its parts. |
| `mkdir -p #{folder}/Sorted` | A command, with a Ruby value inside: `#{…}` in a command line puts the value in. |
| `Dir.glob(…).each do \|photo\|` … `end` | A loop: for every file ending in .jpg or .JPG, `photo` holds its path and the lines up to `end` run. |
| `name = File.basename(photo).sub(…)` | Ruby: the file name, with .JPG at the end changed to .jpg. |
| `mv #{photo} #{folder}/Sorted/#{name}` | A command: moves the file. |

*Summer Trip.jpg* moved as one file, with no quote marks anywhere. That is the rule that keeps you safe: **a value in a command is always exactly one argument**. It is never split at a space, never read as a pattern, and never read as another command. Try it:

```ruby
#!/usr/bin/env rsh
name = "Summer Trip; rm -rf ~"
echo #{name}
```

This prints `Summer Trip; rm -rf ~` and does nothing else. In a careless shell script the `;` would end one command and start the next, and the next one would delete your home folder. A file name from the web, from a download or from someone else's folder can hold anything; in rsh it stays a name.

> **Tip:** A list gives one argument per item: with `photos = Dir.glob("*.jpg")`, the line `ls -l #{photos}` lists each photo, each one whole.

### 5. When a command fails

Every command ends with an exit status, 0 when it worked. When a command in an rsh script ends with anything else, the script stops there and tells you the file, the line and the number. Here is `copy.rsh`, run when there is no folder called Backup:

```ruby
#!/usr/bin/env rsh
cp ~/Documents/notes.txt ~/Backup/
puts "Notes copied."
```

```
cp: cannot create regular file '/home/aliceliddell/Backup/': Not a directory
rsh: ./copy.rsh:2: cp exited 1
```

The first line is what `cp` said; the second is rsh telling you that line 2 failed, so `Notes copied.` was never printed. A script never carries on as if a step had worked.

To decide yourself what happens, put the command between `begin` and `rescue`. Ruby calls this *rescuing* a failure:

```ruby
begin
  cp -r ~/Documents ~/Backups
  puts "Saved a copy."
rescue => e
  puts "The copy did not work: #{e.stderr.strip}"
  exit 1
end
```

If `cp` works, you see `Saved a copy.` If it fails, the lines after `rescue` run: `e` holds what went wrong, and `e.stderr` is what the command said about it. `exit 1` ends the script and reports the failure.

A command used as a question, after `if` or `unless`, never stops the script; it is just *yes* or *no*:

```ruby
if grep -q milk ~/Documents/notes.txt
  puts "Milk is on the list."
end
```

## A worked example

Alice wants a copy of Documents in a folder named after today, and a clear sentence when something stops it. She writes `backup.rsh`:

```ruby
#!/usr/bin/env rsh
# Copies Documents into a folder in Backups named after today.
# How: ./backup.rsh
today = Time.now.strftime("%Y-%m-%d")
target = File.join(ENV.fetch("HOME"), "Backups", today)
mkdir -p #{target}
begin
  cp -r ~/Documents #{target}
  puts "Saved a copy in Backups/#{today}."
rescue => e
  puts "The copy did not work: #{e.stderr.strip}"
  exit 1
end
```

`Time.now.strftime("%Y-%m-%d")` is Ruby for today's date, as 2026-11-04. She runs `wol script check backup.rsh`, which answers `backup.rsh: rsh script, parses and rsh is clean`, then `wol script run backup.rsh`, and sees `Saved a copy in Backups/2026-11-04.` On a computer without a Documents folder the same script says `The copy did not work: cp: cannot stat '/home/aliceliddell/Documents': No such file or directory` and ends with exit status 1.

## If something goes wrong

| What you see | What it means and what to do |
|---|---|
| `zsh: permission denied: ./hello.rsh` | The file may not run yet. Type `chmod +x hello.rsh` once, or use `wol script run hello.rsh`. |
| `rsh: ./copy.rsh:2: cp exited 1` | The command on line 2 failed and the script stopped. The line above it is what the command said. Fix the cause, or rescue it (step 5). |
| `rsh: lss: command not found` | A command name is misspelled, or the program is not installed. |
| `index 0 outside of array bounds` with `IndexError` | The script needs an argument and got none. Add it after the name: `./count.rsh Holiday`. |
| `a value in a command is nil` | A variable you put in a command holds nothing (Ruby calls that *nil*). rsh stops instead of running the command with a missing word. Check where the variable is set. |
| A line you meant as a command ran as Ruby | Its first word is also a variable or method in your script, such as `date` after `date = …`. Put `$ ` in front to force a command: `$ date +%A`. |
| `undefined method` with `NoMethodError` | A Ruby word is misspelled, or it is not in the Ruby that comes with rsh. For your home folder, write `ENV.fetch("HOME")`. |
| `bad.rsh:3:0: syntax error, unexpected end of file` | rsh stopped understanding the file at line 3: usually an `end` or a closing quote mark is missing above it. `wol script check` shows this without running anything; fix the line, save, check again. |

> **Tip:** A file ending in `.rb` mixes Ruby and commands the same way when rsh reads it, so start it with `#!/usr/bin/env rsh`. To keep a file as plain Ruby, with no line ever run as a command, make `# rsh: ruby` the line right after the shebang.

## Where to learn more

[The rsh page](https://github.com/wonderlandos/rsh) shows more of what rsh does, with each job written both as a shell script and in rsh. [Scripts that change settings](/manual/ruby-script) shows what WolOS asks before a script changes a setting, and [Scripts and shells](/manual/scripts-and-shells) how to run a script on another computer.
