Ruby
Introduction to Ruby
Where to get Ruby
Ruby on the command line
$ ruby -v
ruby 2.0.0p247 (2013-06-27 revision 41674) [universal.x86_64-darwin13]
The -h flag will provide a list of all the command line options of Ruby.
$ ruby -h
...
Mostly we are going to create files in Ruby, but it is good to know that you can also create simple one-liners without creating a file.
$ ruby -e 'puts 19+23'
42
irb - Interactive Ruby
irb on Linux and Mac, fxri on Windows
Before starting to write scripts, let's take a look at irb, the Interactive Ruby Shell. A REPL (Read Evaluate Print Loop) that can help you explore the languages. Just type irb on the command line and it will give you its own prompt. There you can type in ruby statements.
$ irb
irb(main):001:0> 42
=> 42
irb(main):002:0> 'hello'
=> "hello"
irb(main):003:0> 19+23
=> 42
irb(main):004:0> puts 42
42
=> nil
irb(main):005:0> a = 19
=> 19
irb(main):006:0> b = 23
=> 23
irb(main):007:0> a+b
=> 42
irb(main):008:0> exit
$
Simply typing in numerical or string expressions will print the result. Calling the puts ruby function will also print to the screen, but then the REPL will print the "result" of puts, which is nil. We can assign values to variables, and then use them in other expressions. Finally exit will let us leave the Interactive Ruby session.
Hello world
- "
- '
- puts
The required "Hello World!" example. Create the file called hello.rb with the following content. Then go to the command line, to the same directory where you have the file hello.rb saved and type ruby hello.rb. It will print "Hello World!" to your screen.
What you can see here is that if you would like to have some text in your program you need to put them between quotes (") and you can use the puts
function
to print something to the screen. Any text between quotes is called a "string".
puts "Hello World!"
ruby hello.rb
Just to clarify, unlike in some other programming languages, in Ruby you can use either double-quotes or single-quotes to mark the beginning and the end of a string, but of course it has to be the same on both sides.
puts 'Hello World!'
Ruby development environment
Any plain text editor will do, but there are also IDEs.
Linux
- vim
- emacs
- gedit
Windows
- NotePad++
Create variable and print content using puts
- =
The next step after greeting the world is greeting the programmer.
However, before we get there we would like to create a variable.
We created a variable called person
and put the name of the programmer in it.
We also say we assigned a value to a variable.
For whatever reason most of the programmer are called Foo
so we'll go with that name here too.
We then use puts
to print out the content of the variable.
This will print Foo
to the screen.
person = "Foo"
puts person
Hello Foo using puts
- puts
- ,
It is not enough to print the name of the programmer, we would also want to greet the programmer.
We print the first part of the message, the variable, and the last part of the message in a single call to puts
.
We need to separate these parts with a comma (,
).
person = "Foo"
puts "Hello ", person, ", how are you?"
We got everything on the screen, but we are slightly disappointed as puts
put each part of our message on a separate line. That's not really nice.
Hello
foo
, how are you?
In more technical terms, puts
printed a newline
after printing each one of its parameters.
Hello Foo - print
One way to improve the situation is to use the print
function instead of the puts
function to print to the screen.
That gave us a much nicer result as it did not print a newline after each item thereby moving the next one to the next line.
However, it also did not print a newline at the end of the whole print operation, which would have been also less convenient.
So in order to make it nicer we had to include \n
at the end of our message to make it nicer.
\n
represent a newline so when we include \n
in whatever we print the next thing will be printed on the following line.
person = 'Foo'
print "Hello ", person, ", how are you?\n"
Hello Foo, how are you?
Ruby variable interpolation
To further improve the situation we are going to use the interpolation capabilities of Ruby.
We can insert a hashmark (#
) followed by a variable between a pair of curly braces in a string.
Ruby will replace that whole construct with the value of the variable.
person = "Foo"
puts "In double quotes #{person}"
puts 'In single quotes #{person}'
Here however the behavior of double-quote and single-quote is different. The interpolation only works in double-quoted strings. Single-quoted strings will just include the hash-mark, the opening curly, the variable name and the closing curly.
In double quotes Foo
In single quotes #{person}
Hello Foo - puts with interpolation
Putting the interpolation to use, here is how we greet our programmer using puts
and a variable interpolated in a double-quoted string.
person = 'Foo'
puts "Hello #{person}, how are you?"
Hello foo, how are you?
Hello Foo STDIN read
- STDIN
- gets
What if the person is not called Foo? What if we would like to ask the users of their name and greet them that way? We can do this by reading from the keyboard which is, in technical terms called Standard Input or STDIN.
This is how we do it:
print "Type in your name: "
person = STDIN.gets
puts "Hello #{person}, how are you?"
Here is what happens when I run the program:
ruby examples/intro/hello_foo_stdin.rb
It prints:
Type in your name:
Then I type in Bar
(another well known name in the programmer community) and press ENTER and this is the output I see:
Hello Bar
, how are you?
Why is it printed in two rows?
Because after we typed in Bar we pressed ENTER to tell the computer that we are done. However that ENTER generates a newline and that is included
in the text our program receives. So in the variable person
we have a newline at the end and when we print it, the next thing will be shown
on a new line.
Hello Foo STDIN and strip
- STDIN
- gets
- strip
The solution is to remove the newline from the end of the string. We can do this using the strip
method:
print "Type in your name: "
person = STDIN.gets.strip
puts "Hello #{person}, how are you?"
Now if we run the program and identify as Bar
, the output will look the way we wanted:
Hello Bar, how are you?
Hello Foo ARGV
- ARGV
Finally we could also expect the users to supply their name when they run the program. For this we'll use the ARGV array that contains all the values that were passed to the program on the command line.
We'll take the first element of the array (that has index 0) and use that:
person = ARGV[0]
puts "Hello #{person}, how are you?"
Run like this:
ruby examples/intro/hello_foo_argv.rb Bar
This is the output:
Hello Bar, how are you?
Interpolation with expression
- interpolation
- capitalize
Ruby allows us to include more complex expression in the interpolation, for exampl to call the capitalize
method of a string.
It is nice, but don't put anything complex in there!
person = 'foo'
puts "Hello #{person.capitalize}, how are you?"
Hello Foo, how are you?
Ruby conditionals - if statement
- if
x = 2
if x > 1
puts "x is bigger than 1"
end
if x > 3
puts "x is bigger than 3"
end
Ruby conditionals - if else statement
- else
x = 1
y = 2
if x == y
puts "They are equal"
else
puts "They are not equal"
end
Ruby path to current script
- $0
puts $0
Ruby command line arguments ARGV
- ARGV
We already saw how to use
puts ARGV
puts "------"
puts ARGV[0]
puts ARGV[1]
puts "------"
ARGV.each do|a|
puts "Argument: #{a}"
end
$ ruby examples/intro/cli.rb a b d
a
b
d
------
a
b
------
Argument: a
Argument: b
Argument: d
Variable Types in Ruby
- class
- type
- Integer
- Float
- String
- NilClass
- TrueClass
- FalseClass
- Array
- Hash
height = 187
puts height.class # Integer
weight = 87.3
puts weight.class # Float
text = "Some text"
puts text.class # String
empty = nil
puts empty.class # NilClass
debug = true
puts debug.class # TrueClass
debug = false
puts debug.class # FalseClass
names = ["Anna", "Bea", "Cecil", "David"]
puts names.class # Array
grades = {"math" => 99, "biology" => 87}
puts grades.class # Hash
Resources
Ruby Arrays
Ruby Arrays intro
- Array
- each
- do
names = ["Anna", "Bea", "Cecil", "David"]
puts names.class # Array
puts ''
puts names
puts ''
puts names[0]
puts ''
puts names[-1]
puts ''
puts names[0,3] # range
puts ''
names.each do |name|
puts name
end
[1, 2, 3].each do |number|
puts number
end
Hashes
Ruby empty Hash
h = Hash.new
puts h
Ruby Hash add key-value pairs
h = Hash.new
puts h
h["fname"] = "Foo"
h["lname"] = "Bar"
puts h
Ruby hash list keys
h = Hash.new
puts h
h["fname"] = "Foo"
h["lname"] = "Bar"
puts h
h.keys.each do|key|
puts "#{key} - #{h[key]}"
end
Functions
Why create functions?
- Code reuse
- Easier organization of the code
- Less things to keep in mind at once
- Easier maintenance
- Easier testing
Before we get into how to create and use function, let's talk a bit about why you'd want to create them?
There are several reasonse mentioned here.
One of the is code reuse. It is much better to create a function and use it in several places in your code than to copy and paste the same several lines. For one it will mean you have less code to maintain. Consider this:
You wrote some code to do some work. You need it in several places. So you copy the code. Then a month later you find a bug in one of these copies and fix it. Will you remember to go to all the other copies and fix it there too or will you wait till someone uses that part of the application and encounters the bug?
However, let's be positive positive and instead of a bug what will happen if you find a way to do the same job 10 times faster? Will you now go and replace all the copies?
If you had the code in a function then you only need to fix the bug or update to the faster version in one place and you get the impact everywhere.
Hello World in a function
def hello_world
puts "Hello World!"
end
puts "Before"
hello_world
puts "After"
Before
Hello World!
After
Hello Person in a function
def hello_person(name)
puts "Hello #{name}!"
end
puts "Before"
hello_person "Foo"
hello_person("Bar")
puts "After"
Before
Hello Foo!
Hello Bar!
After
Simple function
def compute_area(width, height)
area = width * height
return area
end
a = compute_area(2, 3)
puts a
b = compute_area(4, 5)
puts b
Ruby Function with a star (asterisk)
-
splat
-
-
asterisk
-
The
*
is thesplat
operator it is slurpy, it will take any number of parameters -
AKA single splat
def with_splat(*things)
puts things.class
end
with_splat()
with_splat("Name", 23)
Function with a star example
def mysum(*numbers)
sum = 0
numbers.each do | number |
sum += number
end
return sum
end
puts mysum()
puts mysum(42)
puts mysum(2, 3)
puts mysum(1, 7, 8)
Ruby Function with two stars
- AKA double splat
def report(**kw)
puts kw.class # Hash
puts kw
end
report()
report("math" => 99, "biology" => 88)
Filesystem
Ruby makedir
- mkdir
if ARGV.length != 1
puts "Usage: #{$0} DIRNAME"
exit
end
dirname = ARGV[0]
d = Dir.mkdir dirname
puts d
Ruby directory exists
- exists
if ARGV.length != 1
puts "Usage: #{$0} DIRNAME"
exit
end
dirname = ARGV[0]
if Dir.exists? dirname
puts "#{dirname} exists"
else
puts "#{dirname} does NOT exist"
end
Ruby write to file
- write
if ARGV.length != 1
puts "Usage: #{$0} FILENAME"
exit
end
filename = ARGV[0]
File.write(filename, 'My first file')
Ruby append to file
if ARGV.length != 1
puts "Usage: #{$0} FILENAME"
exit
end
filename = ARGV[0]
File.write(filename, 'My second line\n', mode: 'a')
File.open("data.txt", "a") do|fh|
fh.puts "Hello"
end
Ruby list files by using glob
- glob
if ARGV.length != 1
puts "Usage: #{$0} DIRNAME"
exit
end
dirname = ARGV[0]
content = Dir.glob(dirname)
print content
ruby examples/files/glob.rb "*.md"
Time
Ruby current time
now = Time.now
puts now
2022-10-12 10:17:08 +0300
Ruby time string formating with strftime
- strftime
now = Time.now
puts now.utc.strftime("%Y-%m-%d %H:%M:%S")
net/http
API requests
- API
- Net::HTTP
- HTTPbin
In order to be able to interact with web applications one needs to be able to write proper HTTP requests to access the API of the web application.
Ruby has a library called Net::HTTP that can provide the tools to write these call.
httpbin is an excellent web site to try to write HTTP requests against. You can even run it on your own computer if you'd like to avoid making external network calls. It allows you to send various types of HTTP requests and will send you a response in a way that you can verify that your request was sent properly.
Part of the goal of this service is to allow you to generate error conditions and see how your web-client code handles a situation where the remote server returns an error code.
GET URL httpbin
- get_response
- URI
- Net::HTTP
- HTTPSuccess
Let's start with a really simple example:
This is a plain GET requests
require 'net/http'
url = 'https://httpbin.org/get'
uri = URI(url)
# puts(uri)
response = Net::HTTP.get_response(uri)
# puts response.class # Net::HTTPOK
if response.is_a?(Net::HTTPSuccess)
#puts "Headers:"
#puts response.to_hash.inspect
#puts '------'
puts response.body
else
puts response.code
end
The get_response
method of the Net::HTTP class.
The response that we stored in a variable cleverly named response
can be interrogated to see if the response was a success or failure.
It is an instance of a Net::HTTPResponse class.
In case it is a success we print the body of the response and see this:
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Host": "httpbin.org",
"User-Agent": "Ruby",
"X-Amzn-Trace-Id": "Root=1-6381d91a-6f521a3c0fa5af313924005b"
},
"origin": "46.120.8.206",
"url": "https://httpbin.org/get"
}
For the other case see the next example.
GET URL that does not exist (404 error)
Here we deliberately created a URL that does not exist on the HTTPbin web server. In this case the response was not Net::HTTPOK,
so Ruby executed the else
part of our program. The code was 404
and the response message was NOT FOUND
. I am sure you have already
encountered links that returned this error message. BTW you can try to visit the URLs using your regular browser as well to see the same response.
require 'net/http'
url = 'https://httpbin.org/qqrq'
uri = URI(url)
# puts(uri)
response = Net::HTTP.get_response(uri)
# puts response.class # Net::HTTPNotFound
if response.is_a?(Net::HTTPSuccess)
#puts "Headers:"
#puts response.to_hash.inspect
#puts '------'
puts response.body
else
puts response.code
puts response.msg
end
The output was:
404
NOT FOUND
GET URL that pretends not to exist (404)
Previously we got a 404 NOT FOUND
HTTP response because we tried to access a page that really does not exist.
It was easy to generate a 404 error, but it would be a lot harder - effectively impossible to consistently get a web-site to
return other HTTP error messages. eg. While we generally would want to avoid getting a 500 INTERNAL SERVER ERROR
but for testing
purposes (for our client code) we might want to be able to consistently create it.
Luckily HTTPbin provide the service to fake any HTTP status code.
First let's see how does it generate 404 NOT FOUND
message:
require 'net/http'
url = 'https://httpbin.org/status/404'
uri = URI(url)
# puts(uri)
response = Net::HTTP.get_response(uri)
# puts response.class # Net::HTTPNotFound
if response.is_a?(Net::HTTPSuccess)
#puts "Headers:"
#puts response.to_hash.inspect
#puts '------'
puts response.body
else
puts response.code
puts response.msg
end
Here, instead of the /get
end-point we access the /status/CODE
end-point replacing the CODE with the desired HTTP status code.
HTTPbin will respond with that status code.
The output was:
404
NOT FOUND
GET URL that pretends to crash (500)
Generating 500 INTERNAL SERVER ERROR
will be more fun, but we don't have to do anything special. Just send a GET
request to the /status/500
end-point.
require 'net/http'
url = 'https://httpbin.org/status/500'
uri = URI(url)
# puts(uri)
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
#puts "Headers:"
#puts response.to_hash.inspect
#puts '------'
puts response.body
else
puts response.code
puts response.msg
end
The output was:
500
INTERNAL SERVER ERROR
Show request headers
Often you'll be interested to see if you managed to set the headers as expected by an API. For that HTTPbin provides the /headers
end-point.
First let's see what happens if we send a plain request to this end-point?
require 'net/http'
url = 'https://httpbin.org/headers'
uri = URI(url)
# puts(uri)
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
#puts "Headers:"
#puts response.to_hash.inspect
#puts '------'
puts response.body
else
puts response.code
end
This is the header as our Ruby code sent it. (Note the User Agent being Ruby
)
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Host": "httpbin.org",
"User-Agent": "Ruby",
"X-Amzn-Trace-Id": "Root=1-6381e195-0fefc04b79ad1bc14b4688b0"
}
}
Get request headers using Firefox
Just to compare, when I visited this url using Firefox, my default browser I get the following results:
{
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.5",
"Host": "httpbin.org",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0",
"X-Amzn-Trace-Id": "Root=1-6394b5d7-395b7a2a377ea2df7dd4dbe8"
}
}
Here the User-Agent is more detailed.
Get request headers using curl
Using curl
$ curl https://httpbin.org/headers
I got the following response:
{
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.81.0",
"X-Amzn-Trace-Id": "Root=1-6394b68a-65bd2c145c2304274a314120"
}
}
Set request headers
- initheader
Finally we get to the point where we can set the headers in the request. Actually it is pretty easy to do so.
We just need to pass a new parameter called initheader
with a hash of the header keys and values we would like to set.
This way we can add new fields to the header and we can replace existing ones. For example here we set the User-Agent
to Internet Explorer 6.0
.
require 'net/http'
url = 'https://httpbin.org/headers'
uri = URI(url)
# puts(uri)
response = Net::HTTP.get_response(uri, initheader = {
"Accept" => "json",
"Auth" => "secret",
"User-Agent" => "Internet Explorer 6.0",
})
if response.is_a?(Net::HTTPSuccess)
#puts "Headers:"
#puts response.to_hash.inspect
#puts '------'
puts response.body
else
puts response.code
end
In the response we can see that the fields were set as expected.
{
"headers": {
"Accept": "json",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Auth": "secret",
"Host": "httpbin.org",
"User-Agent": "Internet Explorer 6.0",
"X-Amzn-Trace-Id": "Root=1-6394bdbc-24b60d3376d56a493049d2e5"
}
}
As you might know every time you access a web site it will log the request in a log file, usually including the User-Agent as well. This way you could generate lots of request to a web site making their stats show that Internet Explorer 6.0 is back in popularity.
Don't do it!
Sending a POST request
- POST
- post_form
There is more to do with GET
requests, but before we go on, let's also see how to send simple POST
requests.
First of all HTTPbin expects the POST
requests to arrive to a different end-point called, /post
.
In order to send a POST
request we call the post_form
method. Pass the URI to it and a hash of the data.
The hash can be empty but it mist be provided. In our case I just provided it with two keys-value pairs.
require 'net/http'
url = 'https://httpbin.org/post'
uri = URI(url)
response = Net::HTTP.post_form(uri, {
"name" => "Foo Bar",
"height" => 175,
},
)
if response.is_a?(Net::HTTPSuccess)
puts response.body
else
puts response.code
end
This is the response. HTTPbin was kind enough to send back the form-data so we can verify that it was sent and received properly.
{
"args": {},
"data": "",
"files": {},
"form": {
"height": "175",
"name": "Foo Bar"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Content-Length": "23",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Ruby",
"X-Amzn-Trace-Id": "Root=1-6394c45b-3378e8395614f5c35db23677"
},
"json": null,
"origin": "46.120.8.206",
"url": "https://httpbin.org/post"
}
Setting header in POST request
If we also would like to set fields in the header we need a slightly more complex way of writing this:
First we need to create a request object using Net::HTTP::Post
.
Then add the form data using the set_form_data
method.
Finally add a few key-value pairs directly to the request.
Then we need to start
the HTTP session and send the request using the request
method.`
When we called start
we had to supply the hostname and the port manually and because our URL is https (and not http)
we also need to set :use_ssl => true
in order for this to work.
require 'net/http'
url = 'https://httpbin.org/post'
uri = URI(url)
req = Net::HTTP::Post.new(uri)
req.set_form_data(
"name" => "Foo Bar",
"height" => 175,
)
req['User-Agent'] = "Internet Explorer 6.0"
req['X-Answer'] = 42
response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) do |http|
http.request(req)
end
if response.is_a?(Net::HTTPSuccess)
puts response.body
else
puts response.code
end
{
"args": {},
"data": "",
"files": {},
"form": {
"height": "175",
"name": "Foo Bar"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Content-Length": "23",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Internet Explorer 6.0",
"X-Amzn-Trace-Id": "Root=1-6394ce2e-2722ccb67d44f1ee1691a05a",
"X-Answer": "42"
},
"json": null,
"origin": "46.120.8.206",
"url": "https://httpbin.org/post"
}
Debugging a failed request
In the previous example you saw that I had to set :use_ssl => true
, but it took some time to figure it out.
The main problem was that in case of failure my previous code only printed the HTTP status code that was 400.
I read the documentation several times and tried various combinations of the code till it occurred to be that maybe
there is some hint in the body
of the response. So I changed the code to print that in case of failure.
require 'net/http'
url = 'https://httpbin.org/post'
uri = URI(url)
req = Net::HTTP::Post.new(uri)
req.set_form_data(
"name" => "Foo Bar",
"height" => 175,
)
req['User-Agent'] = "Internet Explorer 6.0"
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(req)
end
if response.is_a?(Net::HTTPSuccess)
puts response.body
else
#puts response.code
puts response.body
end
This was the response:
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
</body>
</html>
That allowed me to understand that the issue is around the use of ssl: http vs https. First I tried the same code replacing the URL by one that uses http:
url = 'http://httpbin.org/post'
This worked.
At this point I understood I need to search for something about ssl, and that's how I found out that I had to pass :use_ssl => true
.
So remember, at least for debugging, it can be useful to print the content of the body even in the case of error.
GET URL
require 'net/http'
url = "https://code-maven.com/ruby"
uri = URI(url)
puts(uri)
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
puts response.body
else
puts response.code
end
Other
Ruby - Execute external command (system)
- system
res = system("ls -l")
puts "\n"
puts res
Ruby - Execute external command (system) - failure
- system
res = system("ls -l abc")
puts "\n"
puts res
res = system("blabla")
puts "\n"
if res.nil?
puts "res is nil"
end