Programming and Scripting :: Flua string parsing



I've always had trouble with string parsing....it's one of those things that seems like it would be simple, but i never quite get the syntax right.

I'm building a flua gui for the Fluxbox keys file, and having trouble separating the parts of a line.  I've tried various things, and the closer i get to what seems to work, the more unstable the script becomes (getting segmentation faults).

My current parser function (a big mess right now):
Code Sample
function get_line()
-- read from the selected keys file line
if display.value > 0 then
-- all keys
keys_string = gsub(display:text(display.value),":.*","")
--key_string = keys_string
if strfind(strlower(keys_string),"none ") then
mod_choice.value = 0
--new_key_string = gsub(keys_string,"NONE ","")
--else
--new_key_string = keys_string
--key_string = gsub(key_string,"NONE ","")
end

for modnum = 1,9 do
if strfind(keys_string,"Mod"..modnum.." ") then
mod_choice.value = modnum
--key_string = gsub(key_string,"Mod"..modnum,"")
--modnum = modnum+1
end
end

if strfind(strlower(keys_string)," shift ") then
shift.value = 1
--key_string = gsub(key_string,"Shift","")
else shift.value = 0 end
if strfind(strlower(keys_string)," control ") then
ctrl.value = 1
--key_string = gsub(key_string,"Control","")
else ctrl.value = 0 end
-- just the key with no modifiers
--key_string = gsub(keys_string,"* ","")
-- string including just the fluxbox and shell commands
com_string = gsub(display:text(display.value),".*:","")
if strfind(strlower(com_string),"execcommand ") then sh_command.value = gsub(com_string,"ExecCommand ","") else sh_command.value = "" end
--print("com_string="..com_string)
end
--key_string = strfind(keys_string,".* ",-1)
--print("key_string="..key_string)
end

Some of the comments are comments, and some are failed or in-progress tests.  I apologize if it's confusing....it's confusing to me too =o)

Essentially, this is what I'm trying...
Load a keys file into a hold_browser. When the user clicks a line, the contents of that line are loaded into a series of input fields and pulldown menus.  I've had little trouble grabbing Mod#, Shift, Control, and (partially) the command.  I haven't figured out yet how to load the flux_command into the chooser menu, but that's a future hurdle.  The big problem i'm having is grabbing the key without modifiers...at least in a way that doesn't crash.  It would be really nice if i could figure out a way to emulate awk '{print <num>}'

A sample keys file can look something like this:

Mod1 Tab :NextWorkspace
mod1 Shift TAB :prevworkspace  
NONE shift Control f1 :execCommand aterm -e poop
none control SHIFT F12 :ExecCommand aterm -e su -c "blah -five"

There is no set number of fields, no defined order of fields, and case is arbitrary.  I can deal with the case by using strlower(), and i can break up the string into pieces by breaking it down in stages (cutting key_string multiple times).  However, both of these processes seem to break flua after a few clicks.  I don't know if flua has difficulty with stopping what it's doing when the user clicks a new line (or if it doesn't stop at all?), or if i'm just a sloppy scripter.  Regardless, i've tried to slow down on the clicks and it still crashes, so my guess is that flua just doesn't like to set the same variable several times in a short time, or maybe I'm overlapping variables so nothing works.
While typing this I'm forming some ideas, but i'd still llove some input.  This is the most complicated task i've had so far (and it seemed so simple at the beginning).

Maybe I need to set initial variables, and return to them as soon as the user clicks anything?
Should I use a new variable each time a string is broken into a smaller part?

Thank you for any ideas.

For added clarity (or maybe added confusion), here is the complete script as it is now.  Its functionality is limited...it loads a keys file, and has rudimentary line-editing capability.  It does not write anything external...that feature will not be added until everything else works.  There are various print commands included, which i've been using to debug.
Code Sample
-- DEFAULTS
keysfile = getenv("HOME").."/.fluxbox/keys"
ww = 420        -- window width
wh = 300        -- window height
bw = 80        -- button width
bh = 20        -- button height
bvs = bh+5    -- button vertical spacing
bhs = bw+5    -- button horizontal spacing
Fl_Widget.initializers = {textfont = 15, labelfont = 15, labelcolor = 0}
Fl_Input_.initializers = {when = When.changed}
Fl_Button.initializers = {box = Boxtype.thin_up, down_box = Boxtype.thin_down}
Fl_Round_Button.initializers = {box = Boxtype.none, down_box=Boxtype.round_down, type = Buttontype.radio, align = Align.bottom}
Fl_Window.initializers = {box = Boxtype.thin_down, color = 15}

function set_line()
-- modify the keys file line
-- a=mod# b=Control c=Shift d=key e=fluxcommand f=shellcommand
if display.value > 0 then
a = mod_choice.text
if ctrl.value == 1 then b = " Control" else b = "" end
if shift.value == 1 then c = " Shift" else c = "" end
d = " "..key.value
e = " :"..fb_command.text
if fb_command.text == "ExecCommand" then f = " "..sh_command.value else f = "" end
display:set_text(display.value,a..b..c..d..e..f)
end
end

function get_line()
-- read from the selected keys file line
if display.value > 0 then
-- all keys
keys_string = gsub(display:text(display.value),":.*","")
--key_string = keys_string
if strfind(strlower(keys_string),"none ") then
mod_choice.value = 0
--new_key_string = gsub(keys_string,"NONE ","")
--else
--new_key_string = keys_string
--key_string = gsub(key_string,"NONE ","")
end

for modnum = 1,9 do
if strfind(keys_string,"Mod"..modnum.." ") then
mod_choice.value = modnum
--key_string = gsub(key_string,"Mod"..modnum,"")
--modnum = modnum+1
end
end

if strfind(strlower(keys_string)," shift ") then
shift.value = 1
--key_string = gsub(key_string,"Shift","")
else shift.value = 0 end
if strfind(strlower(keys_string)," control ") then
ctrl.value = 1
--key_string = gsub(key_string,"Control","")
else ctrl.value = 0 end
-- just the key with no modifiers
--key_string = gsub(keys_string,"* ","")
-- string including just the fluxbox and shell commands
com_string = gsub(display:text(display.value),".*:","")
if strfind(strlower(com_string),"execcommand ") then sh_command.value = gsub(com_string,"ExecCommand ","") else sh_command.value = "" end
--print("com_string="..com_string)
end
--key_string = strfind(keys_string,".* ",-1)
--print("key_string="..key_string)
end

-- MAIN WINDOW
w_main = Window{ww,wh, "fluxkeys.flua"}
-- MENU
mm = Menu_Bar{0,0,ww,25;textfont=9}

file_open = Menu_Entry{"&File/&Open..."}
function file_open:callback()
keysfile = fl_file_chooser("Open Keys File","keys*",keysfile)
display:load(keysfile)
display.value=1
line_num.value=display.value
get_line()
end

mm:add(file_open)

file_save = Menu_Entry{"&File/&Save"}
mm:add(file_save)

help = Menu_Entry{"&File/&Help"}
function help:callback()
w_help:show()
end
mm:add(help)

file_quit = Menu_Entry{"&File/&Quit"}
function file_quit:callback()
  exit(0)
end
mm:add(file_quit)

edit_new = Menu_Entry{"&Edit/&New Key"}
function edit_new:callback()
display:insert(display.value+1,"Mod1 Shift F12 :Reconfigure")
display.value=display.value+1
line_num.value=display.value
end
mm:add(edit_new)

edit_remove = Menu_Entry{"&Edit/&Remove Key"}
function edit_remove:callback()
display:remove(display.value)
end
mm:add(edit_remove)
-- MENU END

line_num = Int_Input{360,0,60,25,"line:"}
function line_num:callback()
if line_num.value > "" then display.value = line_num.value end
end

-- The main text field, containing the keys file itself
display = Hold_Browser{10,30,ww-20,200}
function display:callback()
line_num.value = display.value
get_line()
--debug
--print(display:text(display.value))
end

-- CHOOSE MODIFIERS
mod_choice = Choice{10,240,64,20}
mod_choice:add(Menu_Entry{"NONE"})
mod_choice:add(Menu_Entry{"Mod1"})
mod_choice:add(Menu_Entry{"Mod2"})
mod_choice:add(Menu_Entry{"Mod3"})
mod_choice:add(Menu_Entry{"Mod4"})
mod_choice:add(Menu_Entry{"Mod5"})
mod_choice:add(Menu_Entry{"Mod6"})
mod_choice:add(Menu_Entry{"Mod7"})
mod_choice:add(Menu_Entry{"Mod8"})
mod_choice:add(Menu_Entry{"Mod9"})
mod_choice.value = 0
ctrl = Round_Button{7,260,64,20;type=Buttontype.toggle}
ctrl_label = Box{10,260,20,20,"Control";align=Align.right}
shift = Round_Button{7,275,64,20;type=Buttontype.toggle}
shift_label = Box{10,275,20,20,"Shift";align=Align.right}

-- CHOOSE KEY
key = Input{80,240,80,20}

-- CHOOSE FLUXBOX COMMAND
fb_command = Choice{170,240,240,20}
fb_command:add(Menu_Entry{"ExecCommand"})
fb_command:add(Menu_Entry{"Reconfigure"})
fb_command:add(Menu_Entry{"Workspace1"})
fb_command:add(Menu_Entry{"Workspace2"})
fb_command:add(Menu_Entry{"Workspace3"})
fb_command:add(Menu_Entry{"Workspace4"})
fb_command.value = 0
sh_command = Input{170,265,240,20}


function mod_choice:callback()
--print(mod_choice.text)
set_line()
end
function fb_command:callback()
if fb_command.text == "ExecCommand" then sh_command:show() else sh_command:hide() end
--print(fb_command.text)
set_line()
end
--mod_choice.callback()
--fb_command.callback()

w_main:end_layout()


-- HELP WINDOW
w_help = Window{ww,wh, "fluxkeys help"}
author = Box{0,wh-bh,ww,20, "Lua FLTK script by mikshaw 2005"}
w_help:end_layout()

w_main:show()

I decided to start rewriting the get_line function from scratch.  So far no segfault, although i still have to figure out how to separate "F7" from "Mod1 Shift F7 :*" or None Control Shift F7 :*".

Also put the fb_command lines into a table so i don't have a huge number of redundant "fb_command:add" commands in there.

New get_line function, without the sigle-key detection.
"fluxbox_commands" is the comma-separated table of fluxbox commands, "BLANK", "ExecCommand", "Restart", "Workspace1", etc...
Eventually i hope to make fluxbox version-specific tables.
Code Sample
function get_line()
-- find parts of selected keysfile line

-- find Mod<num> or "None"
for modnum = 1,9 do
if strfind(strlower(display:text(display.value)),"mod"..modnum.." ") then
mod_choice.value = modnum
break
else mod_choice.value = 0
end
end

-- find "Control"
if strfind(strlower(display:text(display.value))," control ") then
ctrl.value = 1
else
ctrl.value = 0
end

-- find "Shift"
if strfind(strlower(display:text(display.value))," shift ") then
shift.value = 1
else
shift.value = 0
end

-- find "ExecCommand"
if strfind(strlower(display:text(display.value)),":execcommand ") then
fb_command.value = 1
sh_command.value = gsub(display:text(display.value),".*:-ommand ","")
sh_command:show()
else
-- else find other fluxbox commands
sh_command:hide()
for find_flux = 1,num_fb_commands do
if strfind(strlower(display:text(display.value)),":"..strlower(fluxbox_commands[find_flux])) then
print(fluxbox_commands[find_flux])
fb_command.value = find_flux
break
else fb_command.value = 0
end
end
end
--print(display:text(display.value))

end

mikshaw, this looks like a great project.  I don't have the big answers for you, but here are some of my thoughts:

You want to parse a line to find out what components are there so for each component, you can create a check box (like for shift and mod8), a pull down menu item (like for  :prevworkspace or :execcommand), and in input box for things like the execcommand string.  What I would do is parse the strings into two parts: before the command and after.  Is that OK to do or not?  I thought that the keys and modifers came before the :command.

I used the sherlock holmes method to find the key: eleminate everything else, and whatever is left must be the key!

EDIT: modified code to separate out key.

Code Sample
-- test for mikshaw

-- make some test strings

st1 = "Mod1 Tab :NextWorkspace"
st2 = "mod1 Shift TAB :prevworkspace"
st3 = "NONE shift Control f1 :execCommand aterm -e poop"
st4 = 'none control SHIFT F12 :ExecCommand aterm -e su -c "blah -five"'

-- make the parsing function
function parseMyLine(mystring)
  frontstring = strsub( mystring, strfind(mystring, "^[^\:]*"))
  backstring = strsub( mystring, strfind(mystring, ":.*"))

  if strfind( strlower(frontstring), "^%s*none") then cb_none = "true" else cb_none = "false" end
  if strfind( strlower(frontstring), "control") then cb_control = "true" else cb_control = "false" end
  if strfind( strlower(frontstring), "shift") then cb_shift = "true" else cb_shift = "false" end

  if strfind( strlower(frontstring), "mod%d") then
     tempModString = strsub( strlower(frontstring), strfind( strlower(frontstring), "mod%d*"))
     cb_modifier = strsub( tempModString, strfind( tempModString, "%d"))
  else
     cb_modifier = "'no value'"
  end

--strip out everything but the key
  frontstring = gsub(frontstring,"[nN][oO][Nn][Ee]","")
  frontstring = gsub(frontstring,"[Cc][Oo][Nn][Tt][Rr][Oo][Ll]","")
  frontstring = gsub(frontstring,"[Ss][Hh][Ii][Ff][Tt]","")
  frontstring = gsub(frontstring,"[Mm][Oo][Dd]%d*","")
  cb_key = gsub(frontstring, "%s%s*","")

  list_command = strsub( strlower(backstring), strfind(strlower(backstring), ":[^%s]*"))
  list_commandargs = gsub(strlower(backstring), ":[^%s]*", "")

  print("PARSING: ".. mystring)
  print("frontstring = " .. frontstring)
  print("backstring = " .. backstring)
  print("\tnone was " .. cb_none)
  print("\tcontrol was " .. cb_control)
  print("\tshift was " .. cb_shift)
  print("\tMOD was " .. cb_modifier)
  print("\tkey was " .. cb_key)
  print("\tfluxbox command was was " .. list_command)
  print("\tfluxbox command args were " .. list_commandargs)
  print()
end

parseMyLine(st1)
parseMyLine(st2)
parseMyLine(st3)
parseMyLine(st4)

I had orignally split the string into the two parts separated by the colon, and ideally i want to put it back in just in case the shell command contains " shift " or " control " (the spaces are useful, by the way...I notice you omitted them in your script). The biggest problem was arising from creating substrings out of substrings i think....i'm guessing that it was inthe middle of parsing wheni tried to get it to start over with a different line?  I still don't know for sure.  Anyway, my current function seems to do exactly what i need without crashing...EXCEPT for grabbing just the single key without modifiers.  I can't see where that is in your script? Thank you for your work...could you please help me to understand it? =o)

I'm beginning to think that a new table for keys might work, in the same way that the fluxbox_commands table is used.  Rather than type in the name of the key, the user would select it from a (very) long list. The problem with that, other than the fact that the list would contain over 100 entries, is that different keyboards have different keys, and it also wouldn't allow the user to enter keycodes instead of key names.

I retro-actively added the part the separates out the string for the key to my previous, just because I'm devious.  There are three main string functions I think we are both using: strfind, strsub, and gsub.  Strfind looks for a match and return either nil if it finds nothing or a pair of numbers.  strsub returns a substring based on the numbers returned by strfind.  gsub replaces a matched pattern in a string with another pattern.

I would try matching a pattern, say shift.  If that matches, I set some switch.  Then i use gsub to remove that string from the test string.  Once all of the modifiers have been removed from the test string (and the spaces) whatever is left is the key itself.

What I did is a little harder to explain.  It all regular expression stuff.  You can look at the manual to see more.

frontstring = strsub( mystring, strfind(mystring, "^[^\:]*"))
          matches from the start of a string ( "^ ) and includes everything from there on except for a : character ( [^:]* )

backstring = strsub( mystring, strfind(mystring, ":.*"))
          matches from the first : and includes the rest of the string.

Next Page...
original here.