Allow Line Editing When Reading Input from The Command Line

Allow line editing when reading input from the command line

What you are looking for is the “line editing feature” which is provided by libedit on macOS.

In order to use it from a Swift command line tool, you need to

  • #include <readline/readline.h> in the bridging header file,
  • add “libedit.tbd” to the “Link Binary With Libraries” section in the
    “Build Phases” of your target.

Here is a minimal example Swift program:

while let cString = readline("prompt>") {
let line = String(cString: cString)
free(cString)
print(line)
}

Important: You have to run this in the Terminal, it won't work properly in the Xcode debugger console.

Each input line can be edited before entering Return,
similar to what you can do in the Terminal. And with

while let cString = readline("prompt>") {
add_history(cString) // <-- ADDED
let line = String(cString: cString)
free(cString)
print(line)
}

you can even use the up/down arrow keys to navigate to previously entered
lines.

For more information, call man 3 readline in the Terminal.

Here is a possible helper function:

func readlineHelper(prompt: String? = nil, addToHistory: Bool = false) -> String? {
guard let cString = readline(prompt) else { return nil }
defer { free(cString) }
if addToHistory { add_history(cString) }
return(String(cString: cString))
}

Usage example:

while let line = readlineHelper(addToHistory: true) {
print(line)
}

Edit a body of text from the command line in C

There is no way in ANSI C to make a portable line-editor. If you roll your own, you will have to reroll it for every new operating system you want your program to work on.

If I may make a suggestion, I would use a pre-existing library to do all of that hard, platform-specific dirty work, and with that leg-up, learn how to handle things like arbitrary-length input and such. Then, when your code works (and is good), learn how to do all that dirty work, and take away the library-crutch. That way, you're not tackling the whole thing - you're breaking it down into more manageable parts.

Even this is a bit of an oversimplification. It took me quite a while to learn how to handle arbitrary-length input.

Also, know that, if you want your code to be portable, removing the library dependency will mean that, if you want to port it, you'll have to either a) rewrite all that dirty-work code, or b) add the library back in.

To end this all on a joke, this is your brain with libraries:

Pigmaei gigantum humeris impositi plusquam ipsi gigantes vident.

(If I have seen a little further it is by standing on the shoulders of Giants.)
--Isaac Newton

This is your brain without libraries:

If I have not seen as far as others, it is because giants were standing on my shoulders.

--Hal Abelson

How to prompt for user input and read command-line arguments

To read user input you can try the cmd module for easily creating a mini-command line interpreter (with help texts and autocompletion) and raw_input (input for Python 3+) for reading a line of text from the user.

text = raw_input("prompt")  # Python 2
text = input("prompt") # Python 3

Command line inputs are in sys.argv. Try this in your script:

import sys
print (sys.argv)

There are two modules for parsing command line options: optparse (deprecated since Python 2.7, use argparse instead) and getopt. If you just want to input files to your script, behold the power of fileinput.

The Python library reference is your friend.

How do you edit the command line in an external editor?

tl;dr

  • PSReadLine comes with the feature you're looking for, namely the ViEditVisually function, and on Unix-like platforms it even has a default key binding.

  • For the feature to work, you need to set $env:VISUAL or $env:EDITOR to the name / path of your editor executable.

  • As of PSReadLine v2.1, if you also need to include options for your editor - such as -f for gvim - you need to create a helper executable that has the options "baked in".

    • Since creating a helper executable is cumbersome, a custom emulation of the feature, as you have attempted and as refined in zett42's helpful answer, may be the simpler solution for now. zett42's answer additionally links to a Gist that improves the original functionality by preserving cursor positions.

    • GitHub issue #3214 suggests adding support for recognizing executables plus their options in these environment variables.

Details below.



  • The PSReadLine module ships with such a feature, namely the ViEditVisually function.

  • Its default key bindings, if any, depend on PSReadLine's edit mode, which you can set with Set-PSReadLineOption -EditMode <mode>:

    • Windows mode (default on Windows): not bound
    • Emacs mode (default on macOS and Linux): Ctrl-xCtrl-e
    • Vi mode: v in command mode (press Esc to enter it)
  • Use Set-PSReadLineKeyHandler to establish a custom key binding, analogous to the approach in the question; e.g., to bind Alt-e:

    • Set-PSReadLineKeyHandler -Chord Alt+e -Function ViEditVisually
  • For the function to work, you must define the editor executable to use, via either of the following environment variables, in order of precedence: $env:VISUAL or $env:EDITOR; if no (valid) editor is defined, a warning beep is emitted and no action is taken when the function is invoked.

    • E.g., to use the nano editor on macOS: $env:VISUAL = 'nano'

    • The value must refer to an executable file - either by full path or, more typically, by name only, in which case it must be located in a directory listed in $env:PATH

      • Note: .ps1 scripts do not qualify as executable files, but batch files on Windows and shebang-line-based shell scripts on Unix-like platforms do.
    • As of PSReadLine 2.1, including options for the executable - such as --newindow --wait for code (Visual Studio Code) is not supported.

      • Therefore, for now, if your editor of choice requires options, you need to create a helper executable that has these options "baked in"; see examples below.

      • GitHub issue #3214 proposed adding support for allowing to specify the editor executable plus options as the environment-variable value, which is something that Git (which recognizes the same variables) already supports; for instance, you could then define:

        $env:VISUAL = 'code --new-window --wait'


Example configuration with a helper executable for code (Visual Studio Code):

  • Create a helper executable in the user's home directory in this example:

    • On Windows:

      '@code --new-window --wait %*' > "$HOME\codewait.cmd"
    • On Unix-like platforms:

      "#!/bin/sh`ncode --new-window --wait `"$@`"" > "$HOME/codewait"; chmod a+x "$HOME/codewait"
  • Add a definition of $env:VISUAL pointing to the helper executable to your $PROFILE file, along with defining a custom key binding, if desired:

$env:VISUAL = "$HOME/codewait"

# Custom key binding
Set-PSReadLineKeyHandler -Chord Alt+e -Function ViEditVisually

Python: how to modify/edit the string printed to screen and read it back?

I had this same use-case for a command-line application.

Finally found a hack to do this.

# pip install pyautogui gnureadline

import pyautogui
import readline
from threading import Thread

def editable_input(text):
Thread(target=pyautogui.write, args=(text,)).start()
modified_input = input()
return modified_input

a = editable_input("This is a random text")
print("Received input : ", a)

The trick here is use pyautogui to send the text from keyboard. But we want to do this immediately after the input(). Since input() is a blocking call, we can run the pyautogui command in a different thread. And have an input function immediately after that in the main thread.

gnureadline is for making sure we can press left and right arrow keys to move the cursor in a terminal without printing escape characters.

Tested this on Ubuntu 20, python 3.7

Input from the keyboard in command line application

I managed to figure it out without dropping down in to C:

My solution is as follows:

func input() -> String {
var keyboard = NSFileHandle.fileHandleWithStandardInput()
var inputData = keyboard.availableData
return NSString(data: inputData, encoding:NSUTF8StringEncoding)!
}

More recent versions of Xcode need an explicit typecast (works in Xcode 6.4):

func input() -> String {
var keyboard = NSFileHandle.fileHandleWithStandardInput()
var inputData = keyboard.availableData
return NSString(data: inputData, encoding:NSUTF8StringEncoding)! as String
}

Open Editor From Command Line and Fetch Input

The common workflow is:

  • the caller application creates a temporary file;
  • determines the default editor (for Debian-based it would be /usr/bin/editor, for other linuces — the content of shell variable $EDITOR, etc);
  • runs a shell command in a subshell with Kernel#system (not with backticks!);
  • waits for it to exit;
  • determines the exit code, and skips following if it is not 0;
  • reads the content of temporary file, created in step 1 and removes this file.

In ruby that would be like:

▶ f = Tempfile.new 'cocoapods'
#⇒ #<File:/tmp/am/cocoapods20151120-6901-u2lubx>
-rw------- 1 am am 0 nov 20 15:03 /tmp/am/cocoapods20151120-6901-u2lubx
▶ path = f.path
#⇒ "/tmp/am/cocoapods20151120-6901-u2lubx"
▶ f.puts 'This content is already presented in file'
#⇒ nil
▶ f.close # HERE MUST BE ENSURE BLOCK, BUT FOR THE SAKE OF AN EXAMPLE...
#⇒ nil
▶ system "editor #{path}"
#⇒ Vim: Warning: Output is not to a terminal

If you are testing this in console, just type anything, followed by Esc:wq. In real life there will be normal vim (or what the default editor is) opened.

▶ File.read path
#⇒ "GGGGGGGGGThis content is already presented in file\n"

All together:

#!/usr/bin/env ruby

require 'tempfile'

f = Tempfile.new 'cocoapods'
path = f.path
f.puts 'This content is already presented in file'
f.close # HERE MUST BE ENSURE BLOCK, BUT FOR THE SAKE OF AN EXAMPLE...
system "editor #{path}"
puts File.read path


Related Topics



Leave a reply



Submit