Difference between pages "Guacamole 0.8 on SUSE" and "BaBE - Bash By Examples"

From Linuxintro
(Difference between pages)
imported>ThorstenStaerk
 
 
Line 1: Line 1:
= Overview =
+
<metadesc>BaBE - Bash by Examples; Your significant Linux scripting tutorial: Redirection, Piping, conditional execution, user dialogs, loops, process management, backticks, string replacement, pitfalls and much more.</metadesc>
Guacamole is a [[program]] to [[control a Linux desktop]] over the [[network]] in a browser.
+
BaBE - Bash By Examples; Your significant Linux scripting tutorial;;
  
Sometimes in your Linux life, you need to control your servers in the internet with a graphical user interface. This is tedious when you are behind a corporate firewall blocking ssh requests to the public internet. Typical corporate firewalls only allow proxified client access to port 80, 8080 and 443 in the public internet. One way to go is to use a browser to display a Linux desktop. The solution is not, however, to use [[Vnc#for_a_web_browser|VNC for a web browser]], as it will be blocked by corporate firewalls. The solution is [http://guacamole.sourceforge.net/ guacamole].
+
<pic src="http://www.linuxintro.org/images/Bash-scripting-mindmap.jpg" width=70% align=right caption=MindMap />
  
[[File:Snapshot-guacamole.png|250px]]
+
= Hello world =
 +
The easiest way to get your feet wet with a programming language is to start with a program that simply outputs a trivial text, the so-called hello-world-example. Here it is for bash:
 +
* create a file named hello in your home directory with the following content:
 +
[http://en.wikipedia.org/wiki/Shebang_%28Unix%29 #!/bin/bash]
 +
[[echo]] "Hello, world!"
 +
* [[open a console]], enter
 +
cd
 +
chmod +x hello
 +
* now you can execute your file like this:
 +
# ./hello
 +
Hello, world!
 +
* or like this:
 +
# bash hello
 +
Hello, world!
 +
You see - the output of your shell [[program]] is the same as if you had entered the commands into a console.
  
= Quickstart =
+
= calling commands =
This will show you
+
In your shell script you can call every command that you can call when [[opening a console]]:
* how to install guacamole 0.8.3 on SUSE 12.2
+
echo "This is a directory listing, latest modified files at the bottom:"
* how to make this configuration survive a reboot
+
ls -ltr
* how to secure transmission with SSL
+
echo "Now calling a browser"
* how to make the website accessible from behind a firewall (port 80 or 443)
+
firefox
 +
echo "Continuing with the script"
  
Here's what you do as root user:
+
A wonderful job. Super helpful inotamrfion.
* install tomcat and a vncserver
 
yast -i tomcat tightvnc
 
  
== configure VNC server ==
+
= parameters =
Guacamole does the communication between a VNC server and the web browser. So whatever you see in VNC will be in the browser. Let's use gnome as desktop environment:
+
<pre>
* install gnome:
+
echo "Here are all parameters you called this script with: $@"
yast -i gnome-session
+
echo "Here is parameter 1: $1"
* activate gnome for your VNC:
+
echo "Which parameter do you want to be shown? "
cat >> .vnc/xstartup
+
read number
#!/bin/sh
+
args=("$@")
gnome-session
+
echo "${args[$number-1]}"
 +
</pre>
  
== deploy guacamole client ==
+
= return codes =
* download the guacamole webapp from http://sourceforge.net/projects/guacamole/files/current/binary/
+
Every bash script can communicate with the rest of the system by
* deploy it
+
* sending data to [[stdout]]
  # mv guacamole-0.8.3.war /srv/tomcat/webapps/
+
* sending data to [[stderr]]
* surf to http://localhost:8080/guacamole-0.8.3. A folder /srv/tomcat/webapps/guacamole-0.8.3 will be created with some content. We will need that later.
+
* delivering a return code
* although login is not yet possible your browser will show a login screen like that:
+
The return code is 0 if everything worked well. You can query it for the most recent command using $?:
 +
bootstick@bootstick:~$ echo "hello world"; echo $?
 +
hello world
 +
  0
 +
bootstick@bootstick:~$ echo "hello world">/proc/cmdline; echo $?
 +
bash: /proc/cmdline: Permission denied
 +
1
  
[[File:guacamole-login.png|250px]]
+
In bash, true is 0 and false is any value but 0. There exist two commands, true and false that deliver true or false, respectively:
 +
bootstick@bootstick:~$ true; echo $?
 +
0
 +
bootstick@bootstick:~$ false; echo $?
 +
1
  
== install guacamole server ==
+
= line feeds =
* install some [[dependencies]] that the server will need to build with vnc support:
+
Let's look at the following script:
  yast -i LibVNCServer-devel libpng-devel cairo-devel
+
read name
* download guacamole-server from http://sourceforge.net/projects/guacamole/files/current/source/
+
if [ $name = "Thorsten" ]; then echo "I know you"; fi
* unpack it
+
Instead of a semicolon you can write a line feed like this:
  tar xvzf guacamole-server-0.8.3.tar.gz
+
  read name
* build the server:
+
if [ $name = "Thorsten" ]
  cd guacamole-server-0.8.3
+
  then echo "I know you"
./configure && make -j8 && make install
+
  fi
* the following step is ugly; installation and binary do not completely fit so we must do that:
+
And instead of a line feed you can use a semicolon:
  # ln -s /usr/local/lib/libguac.so* /usr/lib64
+
  read name; if [ $name = "Thorsten" ]; then echo "I know you"; fi
# ln -s /usr/local/lib/libguac-client-vnc.so* /usr/lib64
+
If you want to insert a line feed where you do not need one, e.g. to make the code better readable, you must prepend it with a backslash:
* now we start the guacamole daemon
+
  read \
  # guacd
+
  name
guacd[11581]: INFO:  Guacamole proxy daemon (guacd) version 0.8.3
+
  if [ $name = "Thorsten" ]
guacd[11581]: INFO:  Successfully bound socket to host ::1, port 4822
+
  then \
  guacd[11581]: INFO:  Exiting and passing control to PID 11582
+
    echo "I know you"
 +
  fi
  
== configure guacamole ==
+
= storing a command's output =
* create a folder for guacamole's configuration:
+
To read a command's output into a variable use $(), backticks or [[piping]].
  mkdir /etc/guacamole
+
 
* create a file /etc/guacamole/guacamole.properties with the content
+
== $() ==
  # Hostname and port of guacamole proxy
+
arch=$(uname -m)
  guacd-hostname: localhost
+
echo "Your system is a $arch system."
  guacd-port:     4822
+
 
   
+
== backticks ==
  # Location to read extra .jar's from
+
arch=`uname -m`
  lib-directory:  /srv/tomcat/webapps/guacamole-0.8.3/WEB-INF/classes
+
echo "Your system is a $arch system."
   
+
 
  # Authentication provider class
+
== piping ==
  auth-provider: net.sourceforge.guacamole.net.basic.BasicFileAuthenticationProvider
+
[[Piping]] is a very elegant concept in the Linux world. It allows you to take one command's output and use it as input for the next command. Now you can divide tasks into smaller tasks. For example instead of having a program counting all files in a directory you use one program (ls) to ''list'' all files in a directory and one program (wc) to count the lines:
   
+
ls | wc -l
  # Properties used by BasicFileAuthenticationProvider
+
You can also put the output into a variable, in this case $arch:
  basic-user-mapping: /etc/guacamole/user-mapping.xml
+
  uname -m | while read arch; do echo "Your system is a $arch system."; done
* create a file /etc/guacamole/user-mapping.xml with the content
+
 
  <user-mapping>
+
== comparison ==
    <authorize username="user" password="password">
+
The advantage of using backticks over $() is that backticks also work in the sh shell. The advantage of using $() over backticks is that you can cascade them. In the example below we use this possibility to get a list of all files installed with rpm on the system:
      <protocol>vnc</protocol>
+
filelist=$([[rpm]] -ql $(rpm -qa))
          <param name="hostname">localhost</param>
+
You can use the piping approach if you need to cascade in sh, but this is not focus of this bash tutorial.
          <param name="port">5901</param>
+
 
          <param name="password">password</param>
+
== common mistakes ==
    </authorize>
+
Usually unexperienced programmers try something like
</user-mapping>
+
uname -m | read arch
 +
which [http://mywiki.wooledge.org/BashFAQ/024 does not work]. You must embed the read into a while loop.
 +
 
 +
= conditions =
 +
The easiest form of a condition in bash is this '''if''' example:
 +
echo "what is your name? "
 +
  read name
 +
  if [ $name = "Thorsten" ]; then echo "I know you"; fi
 +
Now let's look closer at this, why does it work? Why is there a blank needed behind the [ sign? The answer is that [ is just an ordinary [[command]] in the shell. It delivers a return code for the expression that follows till the ] sign. To prove this we can write a script:
 +
  if true; then echo "the command following if true is being executed"; fi
 +
if false; then echo "this will not be shown"; fi
 +
 
 +
== empty strings ==
 +
An empty string evaluates to false inside the [ ] operators so it is possible to check if a string ''result'' is empty like this:
 +
  # result=
 +
# if [ $result ]; then echo success; fi
 +
  # result=good
 +
# if [ $result ]; then echo success; fi
 +
success
 +
 
 +
== arithmetic expressions ==
 +
You can compare integer numbers like this:
 +
echo "what is your age? "
 +
read age
 +
if (( $age >= 21 )); then echo "Let's talk about sex."; fi
 +
However bash does not understand floating point numbers. To compare floating numbers you will use external programs such as bc:
 +
 
 +
$ if [ $(echo "2.1<2.2"|bc) = 1 ]; then echo "correct"; else echo "wrong"; fi
 +
  correct
 +
$ if [ $(echo "2.1>2.2"|bc) = 1 ]; then echo "correct"; else echo "wrong"; fi
 +
wrong
 +
 
 +
== not equal ==
 +
To check if a variable is NOT equal to whatever, use !=:
 +
  if [ "$LANG" != "C" ]; then echo "please set your system lanugage to C"; fi
 +
 
 +
== common mistakes ==
 +
Common mistakes are:
 +
* to forget the blank behind/before the [ or ] character
 +
* to forget the blank behind/before the equal sign
 +
* see [[what does "unary operator expected" mean]]
 +
 
 +
= Redirections =
 +
To redirect the output of a [[command]] to a file you have to consider that there are two output streams in UNIX, [[stdout,stderr and stdin|stdout and stderr]].
 +
 
 +
= filling files =
 +
To create a file, probably the easiest way is to use [[cat]]. The following example writes text into README till a line occurs that only contains the string "EOF":
 +
cat >README<<EOF
 +
This is line 1
 +
  This is line 2
 +
  This is the last line
 +
  EOF
 +
Afterwards, README will contain the 3 lines below the cat command and above the line with EOF.
 +
 
 +
= loops =
 +
 
 +
== for loops ==
 +
Here is an example for a for-loop. It makes a [[backup]] of all text files:
 +
for i in *.txt; do [[cp]] $i $i.bak; done
 +
The above command takes each .txt file in the current directory, stores it in the [[variable]] $i and copies it to $i.bak. So ''file''.txt gets copied to ''file''.txt.bat.
 +
 
 +
You can also use subsequent numbers as a for loop using the command seq like this:
 +
for i in $([[seq]] 1 1 3); do [[echo]] $i; done
 +
 
 +
== while loops ==
 +
$ while true; do read line; done
 +
 
 +
= negations =
 +
You can negate a result with the ! operator. $? is the last command's return code:
 +
# true
 +
# echo $?
 +
0
 +
# false
 +
# echo $?
 +
  1
 +
  # ! true
 +
  # echo $?
 +
1
 +
# ! false
 +
# echo $?
 +
0
 +
So you get an endless loop out of:
 +
while ! false; do echo hallo; done
 +
 
 +
The following code checks the file /tmp/success to contain "success". As long as this is ''not'' the case it continues checking:
 +
while ! (grep "success" /tmp/success)
 +
do
 +
  sleep 30
 +
done
 +
 
 +
The following code checks if the file dblog.log exists. As long as this is not the case it tries to download it via ftp:
 +
while ! (test -e dblog.log); do
 +
  ftp -p ftp://user:password@server/tmp/dblog.log >/dev/null
 +
  echo -en "."
 +
  sleep 1
 +
done
 +
 
 +
== common mistakes ==
 +
* bash is very picky regarding spaces. There MUST be a space after the ! if it means negation.
 +
 
 +
= sending a process to the background =
 +
To send a process to the [[background]], use the ampersand sign (&):
 +
  firefox & echo "Firefox has been started"
 +
You see a newline is not needed after the &
 +
 
 +
= forking a process =
 +
You can build a process chain using parantheses. This is useful if you want to have two instruction streams being executed in parallel:
 +
(find -iname "helloworld.txt") & (sleep 5; echo "Timeout exceeded, killing process"; kill $!)
 +
 
 +
= functions =
 +
To define a function in bash, use a non-keyword and append opening and closing parentheses. Here a function greet is defined and it prints "Hello, world!". Then it is called:
 +
greet()
 +
{
 +
    echo "Hello, world!"
 +
}
 +
greet
 +
 
 +
= react on CTRL_C =
 +
The command trap allows you to trap CTRL_C keystrokes so your script will not be aborted
 +
<pre>
 +
#!/bin/bash
 +
 
 +
trap shelltrap INT
 +
 
 +
shelltrap()
 +
{
 +
    echo "You pressed CTRL_C, but I don't let you escape"
 +
}
  
== configure tomcat ==
+
while true; do read line; done
* find out your tomcat's user directory:
+
</pre>
# cat /etc/passwd|grep tomcat
 
tomcat:x:116:118:Apache Tomcat:/usr/share/tomcat:/bin/sh
 
: in this case it is /usr/share/tomcat
 
* create a folder .guacamole in your tomcat's user directory:
 
mkdir /usr/share/tomcat/.guacamole
 
* link guacamole.properties into your tomcat's user directories' guacamole folder
 
ln -s /etc/guacamole/guacamole.properties /usr/share/tomcat/.guacamole
 
  
== finishing ==
+
;Note: You can still ''pause'' your script by pressing CTRL_Z, send it to the [[background]] and kill it there. To catch CTRL_Z, replace INT by TSTP in the above example. To get an overview of all signals that you might be able to trap, [[open a console]] and enter
* start a vnc server, as password set password (the vnc password given in user-mappings.xml)
+
kill -l
vncserver
 
* restart your tomcat server
 
/etc/init.d/tomcat restart
 
* point your browser to http://localhost:8080/guacamole-0.8.3
 
* log in as user, password password (the user given in user-mappings.xml)
 
* you should see a screen like this:
 
  
[[File:Guacamole-after-login.png]]
+
= helpful programs =
 +
== awk: read a specific column ==
 +
[[awk]] is a program that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can extract columns from a text. Let's imagine you want to use the [[program]] [[vmstat]] to find out how high the CPU user load was. Here is the output from vmstat:
 +
procs -----------memory---------- ---swap-- -----io---- -system-- -----cpu------
 +
  r  b  swpd  free  buff  cache  si  so    bi    bo  in  cs us sy id wa st
 +
  1  0      0 1304736 190232 885840    0    0    20    8  11  17  1  0 99  0  0
 +
We see the user load is in colum 13, and we only want to print this column. We do it with the following command:
 +
vmstat 5 | awk '{print $13}'
 +
This will print a line from vmstat all 5 seconds and only write the column 13. It looks like this:
 +
# vmstat 5 | awk '{print $13}'
 +
 +
us
 +
1
 +
1
 +
0
 +
1
 +
To store the CPU user load into a variable we use
 +
load=$(vmstat 1 2 | tail -n 1 | awk '{print $13}')
 +
What happens here? First vmstat outputs some data in its first line. The data about CPU load can only be rubbish because it did not have any time to measure it. So we let it output 2 lines and wait 1 second between them ( => vmstat 1 2 ). From this command we only read the last line ( => tail -n 1 ). From this line we only print column 13 ( => awk '{print $13}' ). This output is stored into the variable $load ( => load=$(...) ).
  
* restart after reboot
+
== grep: search a string ==
* next steps: SSL
+
[[grep]] is a [[program]] that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can extract lines that contain a string or match a [[regex]] pattern. Let's imagine you want all external links from www.linuxintro.org's main page:
* next steps: proxypass
+
wget -O linuxintro.txt http://www.linuxintro.org
 +
grep "http:" linuxintro.txt
  
= TroubleShooting =
+
== sed: replace a string ==
 +
[[sed]] is a [[program]] that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can replace a string by another one. Let's imagine you want to print your distribution's name, but lsd_release outputs too much:
 +
# lsb_release -d
 +
Description:    openSUSE 12.1 (x86_64)
 +
You want to remove this string "Description" so you replace it by nothing:
 +
lsb_release -d | sed "s/Description:\t//"
 +
openSUSE 12.1 (x86_64)
  
== invalid login ==
+
Once you understand [[regular expressions]] you can use sed with them:
* now the problem is that tomcat does not know where to find the Authentication class:
 
  
/var/lib/tomcat6/webapps/guacamole/WEB-INF/classes/net/sourceforge/guacamole/net/basic/BasicFileAuthenticationProvider.class
+
* to replace protocol names for a given port (in this case 3200) in /etc/services:
 +
sed -ri "s/.{16}3200/sapdp00 3200/" /etc/services
 +
* if you have an [[apache]] [[web server]] here's how you get the latest websites that have been requested:
 +
cat /var/log/apache2/access_log | sed "<html><acronym title=substitute>s</acronym>;.*<acronym title="for every line remember a substring that is GET, then a blank, then an unknown count of characters that are not a quote">\(GET [^\"]*\)</acronym><acronym title="the part to be substituted will then be followed by an unknown number of arbitrary characters">.*</acronym>;<acronym title="replace this line by the first remembered part">\1;</acronym></html>"
  
is not in /etc/guacamole/guacamole.properties
+
== tr: replace linebreaks ==
 +
[[sed]] is a [[program]] that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can replace a character by another one, even over line breaks.
 +
For example here is how you remove all empty lines from your processor information:
 +
# cat /proc/cpuinfo | while read a; do ar=$(echo -n $a|tr '\n' ';'); if [ "$ar" <> ";" ]; then echo "$ar"; fi; done
  
* so add it
+
== wc: count ==
* cat /etc/passwd gives me a line
+
With the command wc you can count words, characters and lines. wc -l counts lines. For example to find out how many semicolons are in a line, use the following statement:
  tomcat6:x:113:116::/usr/share/tomcat6:/bin/false
+
  while read line; do echo "$line" | tr '\n' ' ' | sed "s/;/\n/g" | wc -l; done
 +
It lets you input a line of text, counts the semicolons in it and outputs the number.
  
ll /usr/share/tomcat6/.guacamole/
+
How does it do this?
total 8
 
drwxr-xr-x 2 root root 4096 Nov 26 07:58 ./
 
drwxr-xr-x 6 root root 4096 Nov 26 07:57 ../
 
lrwxrwxrwx 1 root root  35 Nov 26 07:58 guacamole.properties -> /etc/guacamole/guacamole.properties
 
  
* works now. So the thing is:
+
It reads lines from your keyboard (while read line). It outputs the line (echo "$line"), but it does not output it in the console. The pipe (|) redirects the output to the input stream of the command tr. The command tr replaces the ENTER ('\
** take care that it is called guacamole and not guacamole-0.8.3 (sure?)
+
') by a space (' '). The pipe (|) redirects the output to the input stream of sed. sed substitutes ("s/) the semicolon (;) by (/) a linefeed (\
** make sure the classpath in /etc/guacamole/guacamole.properties is correct, e.g.
+
), globally (/g"). The pipe redirects the output to the input stream of the wc -l command that outputs the count of lines.
# Location to read extra .jar's from
 
lib-directory:  /var/lib/tomcat6/webapps/guacamole/WEB-INF/classes
 
  
== Server error ==
+
If I coiemnucatmd I could thank you enough for this, I'd be lying.
* now I got a server error so I straced guacd:
 
strace -p 15332
 
and saw
 
[pid 20344] open("/usr/lib/x86_64-linux-gnu/libguac-client-vnc.so", O_RDONLY) = -1 ENOENT (No such file or directory)
 
so the problem is that libguac-client-vnc.so is missing.
 
* downloaded java version 1.7.45 and compiled guacamole-client using mvn. But there was no *.so* file in it
 
* so installed libvncserver-dev and rebuild and reinstalled guacamole-server
 
* and there it is, libguac-client-vnc.so
 
* now the error message changed from "server error" to "unauthorized"
 
  
 
= See also =
 
= See also =
* [[guacamole]]
+
* [[bash]]
* [[connect to a Linux computer]]
+
* [[shell]]
* [[guacamole 0.3.0 on Ubuntu 10.04]]
+
* [[scripting]]
* [[guacamole on Debian 6]]
+
* [[bash operators]]
* [[guacamole on Ubuntu 11.10 32bit]]
+
* http://en.wikibooks.org/wiki/Bash_Shell_Scripting
* [[cool things]]
+
* http://ryanstutorials.net/linuxtutorial/scripting.php
* http://guac-dev.org/Debian%20Install%20Instructions
+
* http://wiki.linuxquestions.org/wiki/Bash_tips
* ulteo
+
* http://wiki.linuxquestions.org/wiki/Bash
* http://www.filegott.se/prd/index.php/how-tos/19-how-to-setup-guacamole-in-linux-ubuntu
+
* http://linuxconfig.org/Bash_scripting_Tutorial
* http://guac-dev.org/doc/gug/installing-guacamole.html#idp99200
+
* http://steve-parker.org/sh/intro.shtml
 +
* http://mywiki.wooledge.org/BashFAQ
 +
* http://mywiki.wooledge.org/BashPitfalls
 +
 
 +
[http://www.facebook.com/sharer.php?u=http%3A%2F%2Fwww.linuxintro.org%2Fwiki%2FShell_scripting_tutorial&t=Shell%20scripting%20Tutorial&src=sp Share on Facebook]
 +
 
 +
<stumbleuponbutton />
  
[[Category:Tool]]
+
[[Category:Mindmap]]
[[Category:Webmaster]]
+
[[Category:Learning]]

Revision as of 18:00, 11 January 2016

BaBE - Bash By Examples; Your significant Linux scripting tutorial;;

MindMap

Hello world

The easiest way to get your feet wet with a programming language is to start with a program that simply outputs a trivial text, the so-called hello-world-example. Here it is for bash:

  • create a file named hello in your home directory with the following content:
#!/bin/bash
echo "Hello, world!"
cd
chmod +x hello
  • now you can execute your file like this:
# ./hello
Hello, world!
  • or like this:
# bash hello
Hello, world!

You see - the output of your shell program is the same as if you had entered the commands into a console.

calling commands

In your shell script you can call every command that you can call when opening a console:

echo "This is a directory listing, latest modified files at the bottom:"
ls -ltr
echo "Now calling a browser"
firefox
echo "Continuing with the script"

A wonderful job. Super helpful inotamrfion.

parameters

echo "Here are all parameters you called this script with: $@"
echo "Here is parameter 1: $1"
echo "Which parameter do you want to be shown? "
read number
args=("$@")
echo "${args[$number-1]}"

return codes

Every bash script can communicate with the rest of the system by

  • sending data to stdout
  • sending data to stderr
  • delivering a return code

The return code is 0 if everything worked well. You can query it for the most recent command using $?:

bootstick@bootstick:~$ echo "hello world"; echo $?
hello world
0
bootstick@bootstick:~$ echo "hello world">/proc/cmdline; echo $?
bash: /proc/cmdline: Permission denied
1

In bash, true is 0 and false is any value but 0. There exist two commands, true and false that deliver true or false, respectively:

bootstick@bootstick:~$ true; echo $?
0
bootstick@bootstick:~$ false; echo $?
1

line feeds

Let's look at the following script:

read name
if [ $name = "Thorsten" ]; then echo "I know you"; fi

Instead of a semicolon you can write a line feed like this:

read name
if [ $name = "Thorsten" ]
  then echo "I know you"
fi

And instead of a line feed you can use a semicolon:

read name; if [ $name = "Thorsten" ]; then echo "I know you"; fi

If you want to insert a line feed where you do not need one, e.g. to make the code better readable, you must prepend it with a backslash:

read \
  name
if [ $name = "Thorsten" ]
  then \
    echo "I know you"
fi

storing a command's output

To read a command's output into a variable use $(), backticks or piping.

$()

arch=$(uname -m)
echo "Your system is a $arch system."

backticks

arch=`uname -m`
echo "Your system is a $arch system."

piping

Piping is a very elegant concept in the Linux world. It allows you to take one command's output and use it as input for the next command. Now you can divide tasks into smaller tasks. For example instead of having a program counting all files in a directory you use one program (ls) to list all files in a directory and one program (wc) to count the lines:

ls | wc -l

You can also put the output into a variable, in this case $arch:

uname -m | while read arch; do echo "Your system is a $arch system."; done

comparison

The advantage of using backticks over $() is that backticks also work in the sh shell. The advantage of using $() over backticks is that you can cascade them. In the example below we use this possibility to get a list of all files installed with rpm on the system:

filelist=$(rpm -ql $(rpm -qa))

You can use the piping approach if you need to cascade in sh, but this is not focus of this bash tutorial.

common mistakes

Usually unexperienced programmers try something like

uname -m | read arch

which does not work. You must embed the read into a while loop.

conditions

The easiest form of a condition in bash is this if example:

echo "what is your name? "
read name
if [ $name = "Thorsten" ]; then echo "I know you"; fi

Now let's look closer at this, why does it work? Why is there a blank needed behind the [ sign? The answer is that [ is just an ordinary command in the shell. It delivers a return code for the expression that follows till the ] sign. To prove this we can write a script:

if true; then echo "the command following if true is being executed"; fi
if false; then echo "this will not be shown"; fi

empty strings

An empty string evaluates to false inside the [ ] operators so it is possible to check if a string result is empty like this:

# result=
# if [ $result ]; then echo success; fi
# result=good
# if [ $result ]; then echo success; fi
success

arithmetic expressions

You can compare integer numbers like this:

echo "what is your age? "
read age
if (( $age >= 21 )); then echo "Let's talk about sex."; fi

However bash does not understand floating point numbers. To compare floating numbers you will use external programs such as bc:

$ if [ $(echo "2.1<2.2"|bc) = 1 ]; then echo "correct"; else echo "wrong"; fi
correct
$ if [ $(echo "2.1>2.2"|bc) = 1 ]; then echo "correct"; else echo "wrong"; fi
wrong

not equal

To check if a variable is NOT equal to whatever, use !=:

if [ "$LANG" != "C" ]; then echo "please set your system lanugage to C"; fi

common mistakes

Common mistakes are:

Redirections

To redirect the output of a command to a file you have to consider that there are two output streams in UNIX, stdout and stderr.

filling files

To create a file, probably the easiest way is to use cat. The following example writes text into README till a line occurs that only contains the string "EOF":

cat >README<<EOF
This is line 1
This is line 2
This is the last line
EOF

Afterwards, README will contain the 3 lines below the cat command and above the line with EOF.

loops

for loops

Here is an example for a for-loop. It makes a backup of all text files:

for i in *.txt; do cp $i $i.bak; done

The above command takes each .txt file in the current directory, stores it in the variable $i and copies it to $i.bak. So file.txt gets copied to file.txt.bat.

You can also use subsequent numbers as a for loop using the command seq like this:

for i in $(seq 1 1 3); do echo $i; done

while loops

$ while true; do read line; done

negations

You can negate a result with the ! operator. $? is the last command's return code:

# true
# echo $?
0
# false
# echo $?
1
# ! true
# echo $?
1
# ! false
# echo $?
0

So you get an endless loop out of:

while ! false; do echo hallo; done

The following code checks the file /tmp/success to contain "success". As long as this is not the case it continues checking:

while ! (grep "success" /tmp/success)
do
  sleep 30
done

The following code checks if the file dblog.log exists. As long as this is not the case it tries to download it via ftp:

while ! (test -e dblog.log); do
  ftp -p ftp://user:password@server/tmp/dblog.log >/dev/null
  echo -en "."
  sleep 1
done

common mistakes

  • bash is very picky regarding spaces. There MUST be a space after the ! if it means negation.

sending a process to the background

To send a process to the background, use the ampersand sign (&):

firefox & echo "Firefox has been started"

You see a newline is not needed after the &

forking a process

You can build a process chain using parantheses. This is useful if you want to have two instruction streams being executed in parallel:

(find -iname "helloworld.txt") & (sleep 5; echo "Timeout exceeded, killing process"; kill $!)

functions

To define a function in bash, use a non-keyword and append opening and closing parentheses. Here a function greet is defined and it prints "Hello, world!". Then it is called:

greet()
{
    echo "Hello, world!"
}
greet

react on CTRL_C

The command trap allows you to trap CTRL_C keystrokes so your script will not be aborted

#!/bin/bash

trap shelltrap INT

shelltrap()
{
    echo "You pressed CTRL_C, but I don't let you escape"
}

while true; do read line; done
Note
You can still pause your script by pressing CTRL_Z, send it to the background and kill it there. To catch CTRL_Z, replace INT by TSTP in the above example. To get an overview of all signals that you might be able to trap, open a console and enter
kill -l

helpful programs

awk: read a specific column

awk is a program that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can extract columns from a text. Let's imagine you want to use the program vmstat to find out how high the CPU user load was. Here is the output from vmstat:

procs -----------memory---------- ---swap-- -----io---- -system-- -----cpu------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 1304736 190232 885840    0    0    20     8   11   17  1  0 99  0  0

We see the user load is in colum 13, and we only want to print this column. We do it with the following command:

vmstat 5 | awk '{print $13}'

This will print a line from vmstat all 5 seconds and only write the column 13. It looks like this:

# vmstat 5 | awk '{print $13}'

us
1
1
0
1

To store the CPU user load into a variable we use

load=$(vmstat 1 2 | tail -n 1 | awk '{print $13}')

What happens here? First vmstat outputs some data in its first line. The data about CPU load can only be rubbish because it did not have any time to measure it. So we let it output 2 lines and wait 1 second between them ( => vmstat 1 2 ). From this command we only read the last line ( => tail -n 1 ). From this line we only print column 13 ( => awk '{print $13}' ). This output is stored into the variable $load ( => load=$(...) ).

grep: search a string

grep is a program that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can extract lines that contain a string or match a regex pattern. Let's imagine you want all external links from www.linuxintro.org's main page:

wget -O linuxintro.txt http://www.linuxintro.org
grep "http:" linuxintro.txt 

sed: replace a string

sed is a program that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can replace a string by another one. Let's imagine you want to print your distribution's name, but lsd_release outputs too much:

# lsb_release -d
Description:    openSUSE 12.1 (x86_64)

You want to remove this string "Description" so you replace it by nothing:

lsb_release -d | sed "s/Description:\t//"
openSUSE 12.1 (x86_64)

Once you understand regular expressions you can use sed with them:

  • to replace protocol names for a given port (in this case 3200) in /etc/services:
sed -ri "s/.{16}3200/sapdp00 3200/" /etc/services
  • if you have an apache web server here's how you get the latest websites that have been requested:
cat /var/log/apache2/access_log | sed "<html><acronym title=substitute>s</acronym>;.*<acronym title="for every line remember a substring that is GET, then a blank, then an unknown count of characters that are not a quote">\(GET [^\"]*\)</acronym><acronym title="the part to be substituted will then be followed by an unknown number of arbitrary characters">.*</acronym>;<acronym title="replace this line by the first remembered part">\1;</acronym></html>"

tr: replace linebreaks

sed is a program that is installed on almost all Linux distributions. It is a good helper for text stream processing. It can replace a character by another one, even over line breaks. For example here is how you remove all empty lines from your processor information:

# cat /proc/cpuinfo | while read a; do ar=$(echo -n $a|tr '\n' ';'); if [ "$ar" <> ";" ]; then echo "$ar"; fi; done

wc: count

With the command wc you can count words, characters and lines. wc -l counts lines. For example to find out how many semicolons are in a line, use the following statement:

while read line; do echo "$line" | tr '\n' ' ' | sed "s/;/\n/g" | wc -l; done

It lets you input a line of text, counts the semicolons in it and outputs the number.

How does it do this?

It reads lines from your keyboard (while read line). It outputs the line (echo "$line"), but it does not output it in the console. The pipe (|) redirects the output to the input stream of the command tr. The command tr replaces the ENTER ('\ ') by a space (' '). The pipe (|) redirects the output to the input stream of sed. sed substitutes ("s/) the semicolon (;) by (/) a linefeed (\ ), globally (/g"). The pipe redirects the output to the input stream of the wc -l command that outputs the count of lines.

If I coiemnucatmd I could thank you enough for this, I'd be lying.

See also

Share on Facebook