{"id":97,"date":"2011-08-24T15:48:17","date_gmt":"2011-08-24T19:48:17","guid":{"rendered":"http:\/\/www.jetmore.org\/john\/blog\/?p=97"},"modified":"2012-05-11T15:16:19","modified_gmt":"2012-05-11T19:16:19","slug":"custom-filename-completion-in-termreadlineperl","status":"publish","type":"post","link":"https:\/\/www.jetmore.org\/john\/blog\/2011\/08\/custom-filename-completion-in-termreadlineperl\/","title":{"rendered":"Custom Filename Completion in Term::ReadLine::Perl"},"content":{"rendered":"<p>When we released the last version of our product, I got an IM from a support guy saying that we had broken filename completion in the installer.  My reaction was &#8220;but we don&#8217;t implement filename completion!&#8221;  I ran the installer on the previous release, and sure enough, it supported filename completion.  Wha?<\/p>\n<p>Turns out we were getting filename completion as a side effect of using Term::ReadLine::Perl to do prompting.  Support &#8220;knew&#8221; we supported it, and even had a list of known bugs they passed around, but no one in dev knew about the feature or the bug.  Good times!<\/p>\n<p>Anyway, fixing what we broke on completions, I started looking at the bugs support had been informally floating around.  Essentially there two issues, which caused three separate problems:<\/p>\n<p>1) After every completion, Term::ReadLine::Perl inserted a space character after the path<br \/>\n &#8211; <strong>Problem 1<\/strong>: When auto-completing paths, this meant you had to hit delete after each path completion before you could type the next few characters.<br \/>\n &#8211; <strong>Problem 2<\/strong>: If you didn&#8217;t manually delete the space from the end of the completed filename, our installer would treat the space as part of the filename and tell you the file didn&#8217;t exist.<\/p>\n<p>2) By default, after every completion, Term::ReadLine::Perl &#8220;decorates&#8221; the completed file<br \/>\n &#8211; <strong>Problem 3<\/strong>: If the file you were completing to was a symlink, it would have &#8216;@&#8217; appended to it, and our installer would tell you the file didn&#8217;t exist.<\/p>\n<p>Problems 2 &#038; 3 were fixable with some logic after the user hit enter, and problem 1 wasn&#8217;t that annoying, but I went looking to see if I could fix the actual completion function in Term::ReadLine::Perl.  I did figure it out, but since there seems to be little google-able documentation specific to addressing this in Term::ReadLine::Perl (as opposed to Term::ReadLine::Gnu) I thought I&#8217;d document what I did.<\/p>\n<p>Here&#8217;s my toy app demonstrating the above issues:<\/p>\n<pre class=\"brush: perl; title: ; notranslate\" title=\"\">\r\n$| = 1;\r\nuse Term::ReadLine;\r\nmy $Term = new Term::ReadLine 'Installer';\r\n\r\nmy $file = $Term-&gt;readline('Enter a file path: ');\r\nprint &quot;$file = '$file'n&quot;;\r\n<\/pre>\n<p>The relevant library code is in Term\/readline.pm.  This appears to be a Perl5 wrapper around Perl4 code.<\/p>\n<p>The default filename completion function is called <code>rl_filename_list()<\/code>.  This function has a tunable to turn off the filename decoration (adding &#8220;@&#8221; to symlinks, etc) called <code>$var_CompleteAddsuffix<\/code>, set to &#8220;1&#8221; by default.  I tried turning it off like this at the top of my app:<\/p>\n<pre class=\"brush: perl; light: true; title: ; notranslate\" title=\"\">\r\n$readline::var_CompleteAddsuffix = 0;\r\n<\/pre>\n<p>This worked, in that it turned off decoration.  Unfortunately, the adding of &#8220;\/&#8221; to a completed path is part of decoration, and that&#8217;s functionality I wanted to keep.  So, I guess I need to write my own filename expander.  Here is my toy app with my custom filename expander routine:<\/p>\n<pre class=\"brush: perl; title: ; notranslate\" title=\"\">\r\n$| = 1;\r\nuse Term::ReadLine;\r\nmy $Term = new Term::ReadLine 'Installer';\r\n\r\n# tell Term::ReadLine::Perl to use my filename expander for completion\r\n$readline::rl_completion_function = &quot;main::rl_filename_list_lcl&quot;;\r\n\r\nmy $file = $Term-&gt;readline('Enter a file path: ');\r\nprint &quot;$file = '$file'n&quot;;\r\n\r\nexit;\r\n\r\nsub rl_filename_list_lcl {\r\n    my $pattern = $_[0];\r\n    my @files = (&lt;$pattern*&gt;);\r\n    foreach (@files) {\r\n        if (-d $_) {\r\n            $_ .= '\/';\r\n        }\r\n    }\r\n    return @files;\r\n}\r\n<\/pre>\n<p>The subroutine <code>rl_filename_list_lcl()<\/code> is a slightly modified copy of <code>readline::rl_filename_list()<\/code>.  I removed all the decoration except &#8216;\/&#8217; for dirs, and I removed the check of <code>$var_CompleteAddsuffix<\/code> before decorating (I want it to happen unconditionally).<\/p>\n<p>That solved the decoration issue, but not the &#8220;adds a space after every completion&#8221; issue.  This is controlled by <code>$rl_completer_terminator_character<\/code> being unconditionally set to &#8216; &#8216; in readline.pm&#8217;s <code>complete_internal()<\/code>.  I experimented with setting it to <code>''<\/code> unconditionally, which did fix my problem.  However, I didn&#8217;t want to leave it like that unconditionally for fear of unexpected side effects.  Then I came across this in the code:<\/p>\n<pre class=\"brush: perl; light: true; title: ; notranslate\" title=\"\">\r\n##   $rl_completer_terminator_character -- what to insert to separate\r\n##      a completed token from the rest.  Reset at beginning of\r\n##      completion to ' ' so completion function can change it.\r\n<\/pre>\n<p>Perfect!  I&#8217;m already defining a custom function, so all I have to do is set <code>$readline::rl_completer_terminator_character<\/code> to <code>''<\/code> in it.<\/p>\n<p>Et voila!  My completed toy function with auto-complete behaving the way I want it to:<\/p>\n<pre class=\"brush: perl; title: ; notranslate\" title=\"\">\r\n$| = 1;\r\nuse Term::ReadLine;\r\nmy $Term = new Term::ReadLine 'Installer';\r\n\r\n# tell Term::ReadLine::Perl to use my filename expander for completion\r\n$readline::rl_completion_function = &quot;main::rl_filename_list_lcl&quot;;\r\n\r\nmy $file = $Term-&gt;readline('Enter a file path: ');\r\nprint &quot;$file = '$file'n&quot;;\r\n\r\nexit;\r\n\r\nsub rl_filename_list_lcl {\r\n    my $pattern = $_[0];\r\n    my @files = (&lt;$pattern*&gt;);\r\n    foreach (@files) {\r\n        if (-d $_) {\r\n            $_ .= '\/';\r\n        }\r\n    }\r\n\r\n    # The readline.pm library sets this to ' ' on every completion call, we can\r\n    # force it to '' here but have to do it every time\r\n    $readline::rl_completer_terminator_character = '';\r\n\r\n    return @files;\r\n}\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>When we released the last version of our product, I got an IM from a support guy saying that we had broken filename completion in the installer. My reaction was &#8220;but we don&#8217;t implement filename completion!&#8221; I ran the installer &hellip; <a href=\"https:\/\/www.jetmore.org\/john\/blog\/2011\/08\/custom-filename-completion-in-termreadlineperl\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[5],"tags":[8,14],"_links":{"self":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts\/97"}],"collection":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/comments?post=97"}],"version-history":[{"count":4,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts\/97\/revisions"}],"predecessor-version":[{"id":690,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts\/97\/revisions\/690"}],"wp:attachment":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/media?parent=97"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/categories?post=97"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/tags?post=97"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}