Using Lua scripts (Part 14): CLI commands timers and line editing - brndnmtthws/conky GitHub Wiki
This is a general piece of code for running a cli command and storing the output in a string.
local file = io.popen ("command")
output = file:read ("*a")
file:close ()
Say I wanted to get info about my partitions, I could run the df -h
command like so.
local file = io.popen ("df -h")
output = file:read ("*a")
file:close ()
So if I were to do print (output)
, I would see in the terminal something like:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 19G 4.9G 13G 28% /
udev 3.9G 4.0K 3.9G 1% /dev
tmpfs 1.6G 1.1M 1.6G 1% /run
none 5.0M 0 5.0M 0% /run/lock
none 4.0G 288K 4.0G 1% /run/shm
/dev/sda5 459G 223G 213G 52% /home
/dev/sr0 6.9G 6.9G 0 100% /media/Belk_4e_IRDVD
First problem is that the df -h
command is being run every Conky
cycle (just like if we were to use exec in the conky.conf). To get Lua
to run the command in an execi
type way, execute once per interval,
we need to set up a timer.
SETTING A TIMER.
A Lua timer runs on the output of the Conky object ${updates}
. In the
conky.conf (conky.config = { ... };
) you have the line
update_interval = 1
.
${updates}
simply counts how may times Conky has updated.
We can get this output in Lua through conky_parse
:
updates = tonumber (conky_parse ("${updates}"))
interval = 10
timer = (updates % interval)
In the first line I'm using tonumber so that the output of the
conky_parse
is set as a number instead of a string.
The second line I have set the interval time. This is number of updates, so will only be equal to seconds if your update_interval in the conky.conf is set to 1.
The third line is the counting mechanism. What is going on here is that the number held in the string "updates" is being divided by the number held in the string "interval" and what is being output is the remainder.
So if updates = 6 and interval = 10, 6 / 10 = 0 and 6 / 10ths: timer = 6.
If updates = 38 and interval = 10, 38 / 10 = 3 and 8 / 10ths: timer = 8
Updates = 200, interval = 10: timer = 0.
Updates = 1737, interval = 60: timer = 57
And so on.
With interval = 10, timer will be the numbers 0 to 9 in repeating order. We can then use the output of timer in an if statement:
updates = tonumber (conky_parse ("${updates}"))
interval = 10
timer = (updates % interval)
if timer == 0 then
-- do codeX
end
Timer will be 0 every 10 Conky cycles and at that time codeX will be
run. So this is analogous to ${execi 10 code}
in the conky.conf.
BUT one problem still remains. Unlike ${execi 10}
, the code in
our if statement will not automatically run on Conky start (or Conky
restart after saving the conky.conf). To get around this we have to
think about how the Lua script is being run by the conky.conf.
A simple example Lua script
require("cairo")
require("cairo_xlib")
print("This is outside of the function")
function conky_main()
if conky_window == nil then
return
end
local cs = cairo_xlib_surface_create(
conky_window.display,
conky_window.drawable,
conky_window.visual,
conky_window.width,
conky_window.height
)
cr = cairo_create(cs)
--
print("This is inside the function")
--
cairo_destroy(cr)
cairo_surface_destroy(cs)
cr = nil
end
Run that code in the Conky.conf like this:
conky.config = {
-- Conky settings above.
lua_load = '/path/file.lua',
lua_draw_hook_pre = 'main',
};
conky.text = [[ ]];
Watch the terminal as you launch Conky with conky -c
and you will
see the following:
mcdowall@mcdowall-desktop:~/Desktop$ conky -c ~/lua/conky_test
This is outside of the function
conky: desktop window (e00022) is subwindow of root window (15d)
conky: window type - normal
conky: drawing to created window (0x2e00001)
conky: drawing to double buffer
This is inside the function
This is inside the function
This is inside the function
This is inside the function
This is inside the function
This is inside the function
This is inside the function
This is inside the function
When Conky starts or restarts after a save, the entire Lua script is read from top to bottom, so on Conky start (and restart) we see "This is outside of the function", but ONLY on Conky start.
Once Conky has started, and on every Conky update following, only the
code contained in the function conky_main
is run so we see "This is
inside the function" repeated every Conky cycle.
We can use this to our advantage to activate Lua code on Conky start
NOTE: this feature can be used for lots of other things too.
require("cairo")
require("cairo_xlib")
conky_start = 1
function conky_main()
if conky_window == nil then
return
end
local cs = cairo_xlib_surface_create(
conky_window.display,
conky_window.drawable,
conky_window.visual,
conky_window.width,
conky_window.height
)
cr = cairo_create(cs)
updates = tonumber(conky_parse("${updates}"))
interval = 10
timer = (updates % interval)
if timer == 0 or conky_start == 1 then
print("update number= " .. updates)
print("You will see this at Conky start and then at 10 cycle intervals")
conky_start = nil
end
cairo_destroy(cr)
cairo_surface_destroy(cs)
cr = nil
end
Now you see this in the terminal:
mcdowall@mcdowall-desktop:~/Desktop$ conky -c ~/lua/conky_test
conky: desktop window (e00022) is subwindow of root window (15d)
conky: window type - normal
conky: drawing to created window (0x2e00001)
conky: drawing to double buffer
update number= 1
You will see this at Conky start and then at 10 cycle intervals
update number= 10
You will see this at Conky start and then at 10 cycle intervals
update number= 20
You will see this at Conky start and then at 10 cycle intervals
update number= 30
You will see this at Conky start and then at 10 cycle intervals
NOTE the line conky_start = nil
, as it is important.
Even though the line conky_start = 1
only runs at startup, the
string conky_start
is retained within the Lua script. If we were to
put print (conky_start)
into our function (above where we set it
nil), we would get "1" printed in the terminal. So unless we change the
value of conky_start
, it will retain the value of 1 and the if
evaluation (if timer == 0 or conky_start == 1 then) will be passed
every cycle and the code will be run every cycle.
You could set conky_start
to anything other than 1, but setting it
to nil has the added benefit of deleting the variable. Fewer variables
floating around the better.
NOW that we can control our commands, lets get back to running them!
We ran the df -h
command and stored the output in a string BUT we
want to make the information in that string usable.
For example display the mount point for each item using
cairo_show_text
. Or we want to take the use% for each entry and
display that information as a bar or other indicator. To do this we
need to extract the various bits and pieces out of the string and into
other strings or tables.
One of the best methods for doing this in Lua is the string.find
command. For example, to get the mount points for each partition
listed from df -h
:
local file = io.popen ("df -h")
output = file:read ("*a")
file:close ()
start, finish, mount1 = string.find (output,"%%%s*(/[%d%a%p]*)\n")
start, finish, mount2 = string.find (output,"%%%s*(/[%d%a%p]*)\n",finish)
start, finish, mount3 = string.find (output,"%%%s*(/[%d%a%p]*)\n",finish)
print (mount1)
print (mount2)
print (mount3)
In the terminal, you will see:
/
/dev
/run
These are the first 3 mount points in the list given by df -h
.
THE HARD PART is what to put into the string.find command and when writing code there are usually several ways to get the same result.
There are plenty of resources to learn about string.find
and other
string manipulation commands:
Tutorial, The 5.1 manual
Lua Manual Sting
Manipulation and Lua
Manual Patterns. The
Lua users wiki Lua Manual String Lib
Tutorial.
The way that I use string.find, 3 things are returned from the command in this order.
- The position in the string where that match started.
- The position in the string where the match ended.
- The captured part of the string (the part that contains the info we want).
This is why I have started my lines: start, finish, mount =
,
then the command itself: string.find
.
And then we send the string.find
command all the bits and pieces it
needs inside round brackets ()
The first thing to send it is the
string that contains the information we want to get out, which in this
case is called "output".
The next part is where the matching happens.
LETS look at the results from "df -h"
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 19G 4.9G 13G 28% /
udev 3.9G 4.0K 3.9G 1% /dev
tmpfs 1.6G 1.1M 1.6G 1% /run
none 5.0M 0 5.0M 0% /run/lock
none 4.0G 812K 4.0G 1% /run/shm
/dev/sda5 459G 223G 213G 52% /home
/dev/sr0 6.9G 6.9G 0 100% /media/Belk_4e_IRDVD
We need to identity a feature of the output that we can use in a match. Ideally we need a feature to match before the data we want and a feature to match following the data.
In this case preceding the mount point on each line is the "Use%" number. This is the only place in a line where you see the % symbol, and for every line in the output the mount point for the partition is preceded by a % and a space.
The mount point itself is the last thing in the line. In this case a newline character is \n, so this gives us something to match after the mount point.
NOW we have to look at pattern matching. The standard patterns you can search for are:
. --- (A dot) Represents all characters.
%a --- Represents all letters.
%c --- Represents all control characters.
%d --- Represents all digits.
%l --- Represents all lowercase letters.
%p --- Represents all punctuation characters.
%s --- Represents all space characters.
%u --- Represents all uppercase letters.
%w --- Represents all alphanumeric characters.
%x --- Represents all hexadecimal digits.
%z --- Represents the character with representation 0.
Important: the uppercase versions of the above represent the
complement of the class, eg %U
represents everything except
uppercase letters, %D
represents everything except digits.
There are some "magic characters" (such as %) that have special
meanings. These are: ^ $ ( ) % . [ ] * + - ?
If you want to use those in a pattern (as themselves) you must precede
them by a %
symbol, eg. %% would match a single %.
You can build your own pattern classes by using square brackets, eg:
[abc] ---> Matches a, b or c.
[a-z] ---> Matches lowercase letters (same as %l).
[^abc] ---> Matches anything except a, b or c.
[%a%d] ---> Matches all letters and digits.
[%a%d_] ---> Matches all letters, digits and underscore.
[%[%]] ---> Matches square brackets (had to escape them with %).
The repetition characters are:
+ ---> 1 or more repetitions (greedy).
* ---> 0 or more repetitions (greedy).
- ---> 0 or more repetitions (non greedy).
? ---> 0 or 1 repetition only.
The standard "anchor" characters apply:
^ ---> Anchor to start of subject string.
$ ---> Anchor to end of subject string.
SO lets look at the pattern matching I used:
"%%%s*(/[%d%a%p]*)\n"
.
My data is preceded by %
and that is what I am matching with
%%%s
.
%%
is a % character (remember that % itself is a Lua "magic"
character and needs to be escaped with a preceding %).
%s
is a space character.
CAPTURES. Captures are specified using round brackets ()
.
Looking at my output I see that all of my mount points start with a
back-slash /
. So I put this as the first character in my capture
brackets. One reason for this is that I DON'T want to capture the
"Mounted on" from the first line. Since there is no /
in that line,
it shouldn't match.
After the /
there are many different things that might appear in the
mount point of a partition. In particular you might get any
combination of Numbers, letters and punctuation, which are
represented here with [%d%a%p]*
, ie any combination of numbers,
letters or punctuation repeated 0 or more times.
You shouldn't get any spaces. If you do have spaces in the names of
partitions then you would need to add %s
into the brackets.
So my capture is as follows: (/[%d%a%p]*)
.
Following the capture is /n
because I want to stop the string.find
command when it reaches the end of the line and matches a newline
character.
I will continue with explaining the string.find lines I used as well as other ideas on how to do the data extraction and methods of "automating" the process at a later date.
Here is the output of "df -h" as stored in the string output
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 19G 4.9G 13G 28% /
udev 3.9G 4.0K 3.9G 1% /dev
tmpfs 1.6G 1.1M 1.6G 1% /run
none 5.0M 0 5.0M 0% /run/lock
none 4.0G 288K 4.0G 1% /run/shm
/dev/sda5 459G 223G 213G 52% /home
/dev/sr0 6.9G 6.9G 0 100% /media/Belk_4e_IRDVD
My first data extraction line:
start, finish, mount1 = string.find (output,"%%%s*(/[%d%a%p]*)\n")
print (mount1) --> /
print (finish) --> 101
string.find finished matching my expressions at position 101.
My second data extraction line:
start, finish, mount2 = string.find (output,"%%%s*(/[%d%a%p]*)\n",finish)
print (mount2) --> /dev
Using the value of "finish" from the previous match at the end of this string.find command tells string.find to START searching at that position onward. The default, if not start position is given, is that string.find begins at position 1. The result is that the second mount point is returned.
This could be repeated so on and so forth until you have all the
mountpoints from df -h
.
BUT what if you didn't know how many lines you were dealing with? This will most likely be the case as partitions may be mounted or unmounted.
READING THE OUTPUT LINE BY LINE. This can be an extremely useful technique. This is the general form for the code:
local file = io.popen ("command")
for line in file:lines () do
-- Code
end
file:close ()
What this does is to take each line from the output of the command and
apply the code to it individually. For our df -h
command we could
do this:
local file = io.popen ("df -h")
for line in file:lines () do
print ("This is a line ".. line) -- line is the name of the string that
-- contains each line from the
-- command "df -h"
end
file:close ()
And get this in the terminal:
This is a line Filesystem Size Used Avail Use% Mounted on
This is a line /dev/sda1 19G 4.9G 13G 28% /
This is a line udev 3.9G 4.0K 3.9G 1% /dev
this is a line tmpfs 1.6G 1.1M 1.6G 1% /run
This is a line none 5.0M 0 5.0M 0% /run/lock
This is a line none 4.0G 640K 4.0G 1% /run/shm
This is a line /dev/sda5 459G 223G 213G 52% /home
This is a line /dev/sr0 6.9G 6.9G 0 100% /media/Belk_4e_IRDVD
Looking at the print command (print ("this is a line "..line)
):
I have used .. to join together some text ("This is a line") with a
string called line. The string named line holds each line from the
output of the df -h
command, one after another, in order. This is a
different kind of for loop. The loop runs until it has read every
line and applied any code within the loop then the loop breaks and the
rest of the code continues.
In the loop I simply had the text "This is a line" added onto the front of each line, hence the output.
Let's extract the "Used" value from each line.
For this method I am going to extract the values into a table and I am going to put the table.
updates = tonumber (conky_parse ("${updates}"))
interval = 10
timer = (updates % interval)
if timer == 0 or conky_start == 1 then
used_table = {}
local file = io.popen ("df -h")
for line in file:lines () do
s, f, used = string.find (line,"[%d%p]*%u%s*([%d%p%a]*)%s")
table.insert (used_table, used)
end
file:close ()
conky_start = nil
end
print (used_table[2]) --> 4.9G
This is the table I set up to receive the values used_table = {}
. It
is important that the table is INSIDE the timer-controlled section
because whenever the script runs the line: used_table = {}
, any
contents in the table. So if the table was outside of the timer, it
would be blanked every cycle. BUT data will only be written to the
table once every 10 cycles (interval = 10). So 9 cycles out of 10 the
table will be blank, which is no good.
Here is the string.find line: s, f, used = string.find (line,"[%d%p]*%u%s*([%d%p%a]*)%s")
.
I have shortened the names of the start and finish receiving strings to s and f.
NOTE: if the results of the start and finish position of the
match are of no interest string.match
could be used as follows used = string.match (line,"[%d%p]*%u%s*([%d%p%a]*)%s")
.
string.match only returns the captured part of the string.
BUT I'm going to stick with string.find
.
The first thing we feed the string.find
command is the string that
holds the information we want to look at: line.
Then looking at the output of df -h
, we need to identify the before
and after features to match
/dev/sda1 19G 4.9G 13G 28% /
BEFORE the "Used" value is the "Size" value. The Size value looks to be a combination of numbers and punctuation (decimal point) followed by an upper case letter denoting the unit followed by one or more spaces.
So that is what I put into my matching statement before my capture
brackets: [%d%p]*%u%s*
-
[%d%p]*
= any combination of 0 or more numbers and punctuation. -
%u
= a capital letter. -
%s*
= 0 or more spaces.
AFTER the "Used" value there are always at least one space, and that
is what I will use to end the match after the capture brackets: %s
.
The capture brackets themselves: ([%d%p%a]*)
any combination of 0 or
more numbers, punctuation and letters.
The loop cycles through the lines, extracts the Used value and then:
table.insert (used_table, used)
puts the values into the table in
order.
In the above case the first entry in the table is actually "filesystem" which, by applying the search criteria to the very first line ...
Filesystem Size Used Avail Use% Mounted on
... should be self explanatory.
You could look at what was in the table like this:
table_length = tonumber (#used_table)
for i = 1, table_length do
print (table[i])
end
NOTE: if we only wanted to get the number part of the Used value, there is a catch in this case: if the Used value is greater than 0 the value always ends in an uppercase letter BUT when Used = 0 as in the line ...
none 5.0M 0 5.0M 0% /run/lock
... we only have the 0, and no letter. This, however would work to get
just the number: s, f, used = string.find (line,"[%d%p]*%u%s*([%d%p]*)%u*%s")
.
However, the expression within the capture brackets no longer matches
the first line from df -h
, so print (used_table[1])
prints 4.9 (the
value for the /
partition)`.
The same methods as used to get the data from the output of df -h
can be used to extract information from a web page. The command to use
here would be:
local file = io.popen ("curl 'web address'")
output = file:read ("*a")
file:close ()
NOTE: be careful with your quotation marks. The curl command requires that you enclose the web address you want to access within quotes, but the Lua code also requires that the entire command you want to run be enclosed in quotes. Both types of quotes (double " and single ') can be used to get around this requirement and it doesn't matter which way around you use them -- eg enclose the command in one type and the web address in another.
If you use a text editor like gedit, once you save your file as a .lua, you will see that gedit uses different colors to highlight different parts of the text For me, quoted text in Lua shows pink in Gedit (Gedit = pink, Geany = yellow).
There are several ways you could do this. You can set the web address as a string (for example if you wanted to construct a settings section separate to the main code). This can help you get round any problems related to quotes, also.
updates = tonumber (conky_parse ("${updates}"))
interval = 100
timer = (updates % interval)
if timer == 0 or conky_start == 1 then
used_table = {}
web = "http://crunchbanglinux.org/forums/search/recent/"
file = io.popen ("curl "..web)
output = file:read ("*a")
file:close ()
conky_start = nil
print (output)
end
What gets printed in the terminal is the contents of "output", which is the entire source code for the page.
NOW we can start working on getting the data out. Say we want to get the topmost thread title from the "Active Topics" page on the Crunchbang forum: Crunchbang topic list.
The first thing is to open the page, once as you would view it regularly and once after right clicking somewhere on the page and selecting "View Page Source".
Right now, the top thread in the list is titled: Indefinite Hang on Reboot/Shutdown
.
Go to the source code and search for those exact terms, then look around a bit at the code.
</div>
<div class="main-content main-forum forum-noview">
<div class="main-item odd main-first-item new">
<span class="icon new"><!-- --></span>
<div class="item-subject">
<h3 class="hn"><span class="item-num">1</span> <a href="http://crunchbanglinux.org/forums/topic/21773/indefinite-hang-on-rebootshutdown/">Indefinite Hang on Reboot/Shutdown</a> <span class="item-nav">( <em class="item-newposts"><a href="http://crunchbanglinux.org/forums/topic/21773/indefinite-hang-on-rebootshutdown/new/posts/" title="Go to the first new post since your last visit.">New posts</a></em> )</span></h3>
<span class="item-starter">by <cite>drewdle</cite></span>
</div>
Below that you will see the other topics in the list. The first thing
we need to look for is something unique that identifies this topic as
the first one ... I see: class="item-num">1</span>
.
Before the info I want, and when I search for that expression I find
that it is unique. The only downside is the quotes, which might make
writing a matching expression more difficult. Ignoring the quoted
part: >1</span>
is also unique, so we will use that.
ALSO, directly before the info I want there is this: /">
. This
appears before the title of every entry in the list and can help us to
get only the title out from the code.
And directly after the info I want I see: </a> <span class=
. This
appears to be a constant, so should give us a predictable place to
stop our search.
THE FIRST THING to do is to replace any actual spaces in our
search terms with %s
, so </a> <span class=
becomes
</a>%s<span%sclass=
, and replace any other troublesome characters:
/">
becomes /%p>
(%p
is a punctuation character).
Another thing to look out for is if there are any Lua magic characters that need to be escaped with %.
lets put together our search pattern:
BEFORE the capture brackets we have: >1</span>
followed by any
combination of 0 or more numbers, letters, punctuation and spaces
until we get to /">
which becomes: >1</span>[%d%a%p%s]*/%p>
.
AFTER the capture brackets we have: </a> <span class=
which becomes: </a>%s<span%sclass=
.
INSIDE the capture brackets we are going to match any combination of 0
or more numbers, letters, punctuation and spaces: ([%d%a%p%s]*)
.
The entire code to get the first entry from the "Active topics" page on the Crunchbang forum is then:
updates = tonumber (conky_parse ("${updates}"))
interval = 100
timer = (updates % interval)
if timer == 0 or conky_start == 1 then
used_table = {}
local web = "http://crunchbanglinux.org/forums/search/recent/"
local file = io.popen ("curl "..web)
output = file:read ("*a")
file:close ()
local s, f, topic1 = string.find (output,">1</span>[%d%a%p%s]*/%p>([%d%a%p%s]*)</a>%s<span%sclass=")
conky_start = nil
print (topic1)
end
Which outputs in the terminal:
mcdowall@mcdowall-desktop:~/Desktop$ conky -c ~/lua/conky_test
Conky: desktop window (e0aafa) is subwindow of root window (15d)
Conky: window type - normal
Conky: drawing to created window (0x2a00001)
Conky: drawing to double buffer
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 38635 0 38635 0 0 99067 0 --:--:-- --:--:-- --:--:-- 118k
Indefinite Hang on Reboot/Shutdown
These lines ...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 38635 0 38635 0 0 99067 0 --:--:-- --:--:-- --:--:-- 118k
... show curl working.