Skip to content

Instantly share code, notes, and snippets.

@toofar
Last active May 13, 2016 07:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toofar/24d9ca7398201777da0f2e3b67c7d925 to your computer and use it in GitHub Desktop.
Save toofar/24d9ca7398201777da0f2e3b67c7d925 to your computer and use it in GitHub Desktop.
Bash/Readline function to insert previously used files into the current command line.
maybefile () {
case "$1" in
*/*|/*|*/)
return 0;;
esac
return 1
}
# Parse shell commands (eg from `history`) and look for parameters that look
# like files, that is they contain a '/'. Handles quoting and backslash
# escaping but not variable substition etc.
# If you want it to strip quote and \ chars just put c="" wherever the
# variable quote is set and unset and wherever escape is set.
path_filter() {
nth="$1"
matches=0
mapfile BUF
for ((j="${#BUF[@]}"; j>=0; j--)) ;do
l="${BUF[$j]}"
llen="${#l}"
i=0
escape=''
quote=""
tok=""
while [ $i -le $llen ] ;do
c=${l:$i:1}
case "$c" in
' ')
if [ -z "$quote" ] && [ -z "$escape" ];then
if maybefile "$tok" ;then
matches=$(($matches+1))
if [ -z "$nth" ] || [ $matches -eq $nth ] ;then
printf "%s\n" "$tok"
[ -n "$nth" ] && return
fi
fi
tok=""
c=""
fi
escape=""
;;
'"'|"'")
if [ -z "$quote" ] && [ -z "$escape" ];then
quote="$c"
elif [ -n "$quote" ] && [ -z "$escape" ];then
[ "$quote" = "$c" ] && quote=""
fi
escape=""
;;
"\\")
if [ -z "$quote" ] && [ -z "$escape" ];then
escape="\\"
elif [ -n "$quote" ] && [ -z "$escape" ];then
if [ "$quote" = '"' ];then
escape="\\"
else
escape=""
fi
else
escape=""
fi
;;
*)
escape=""
;;
esac
[ -n "$c" ] && tok="$tok$c"
i=$(($i+1))
done
if [ -n "$tok" ];then
if maybefile "$tok" ;then
matches=$(($matches+1))
if [ -z "$nth" ] || [ $matches -eq $nth ] ;then
printf "%s\n" "$tok"
[ -n "$nth" ] && return
fi
fi
tok=""
fi
done
}
# function to cycle between last path like command line arguments used.
last_file_cycle() {
# Clear our saved variables if HISTCMD changes or the command line has
# changed
if [ -n "$LFC_HISTCMD" ] && [ "$HISTCMD" != "$LFC_HISTCMD" ];then
unset LFC_HISTCMD
unset LFC_ORIG_RLLINE
unset LFC_ORIG_RLPOINT
elif [ "$LFC_NEW_RLLINE" != "$READLINE_LINE" ];then
unset LFC_HISTCMD
unset LFC_ORIG_RLLINE
unset LFC_ORIG_RLPOINT
fi
if [ -z "$LFC_HISTCMD" ];then
LFC_ORIG_RLLINE="${READLINE_LINE}"
LFC_ORIG_RLPOINT="${READLINE_POINT}"
LFC_HISTCMD="$HISTCMD"
LFC_CYCLE=0
fi
LF=`history 100 | path_filter $(($LFC_CYCLE+1))`
LFC_CYCLE=$(($LFC_CYCLE+1))
LFC_NEW_RLLINE="${LFC_ORIG_RLLINE:0:LFC_ORIG_RLPOINT}$LF${LFC_ORIG_RLLINE:LFC_ORIG_RLPOINT}"
READLINE_LINE="${LFC_NEW_RLLINE}"
READLINE_POINT=$((LFC_ORIG_RLPOINT + ${#LF}))
}
bind -x '"\C-f":last_file_cycle'
#!/usr/bin/env python
from __future__ import print_function, with_statement
import shlex
import fileinput
import argparse
import sys
import subprocess
import os.path
from math import log
helpmsg="""
Select path like strings out of input. Eg anything with a path seperator ('/')
in it. Understands shell quoting. See also facebook path picker.
"""
parser = argparse.ArgumentParser(description=helpmsg)
parser.add_argument('files', help='read input from FILES',
metavar='FILES', action='append', nargs='*')
parser.add_argument('-r', '--reverse', help='print last matched path(s) first',
action='store_true', default=False)
parser.add_argument('-n', '--num',
help='prepend each printed line with a number'+
"left padded with zeroes",
action='store_true', default=False)
parser.add_argument('-l', '--limit', help='stop parsing after N paths',
type=int, metavar='N')
parser.add_argument('-q', '--quiet', help='don\'t print out paths '+
'(except for the purposes of --select)',
action='store_true', default=False)
parser.add_argument('-c', '--copy', help='copy results to primary selection '+
'(requires xclip)',
action='store_true', default=False)
parser.add_argument('--clipboard', help='copy results to clipboard '+
'(requires xclip)',
action='store_true', default=False)
# _NOPE_ = not passed, None = passed without arg
parser.add_argument('-s', '--select', help='prompt for selection(s)'+
'from numbered list (eg 1,3-7,10 or substring)',
metavar='selection', nargs='?', default='_NOPE_')
#TODO: could use fnmatch and listdir to support globbing ... but would
#have to walk dirs? Surely there is some lib which handles it
parser.add_argument('-e', '--exists', help='check a file exists before '+
'including it (no glob support)',
action='store_true', default=False)
#parser.add_argument('-f', '--fuzzy', help='start fuzzy matcher to select from '
# +' results (requires dmenu)',
# action='store_true', default=False)
args = parser.parse_args()
lpad_str = "{n:0{width}d} {}"
limit = 0
def display(paths, for_select=False):
if limit and not for_select:
paths = paths[0:limit]
num_paths=len(paths)
fmt_str = "{n:0{width}d} {}" if args.num or for_select else "{}"
for i, p in enumerate(paths):
if not args.quiet or for_select:
print(fmt_str.format(p, n=i, width=int(log(num_paths, 10)+1)))
if args.copy:
p = subprocess.Popen(["xclip"], stdin=subprocess.PIPE)
p.stdin.write("\n".join(paths)+'\n')
if args.clipboard:
subprocess.Popen(["xclip", "--selection=clipboard"],
stdin=subprocess.PIPE)
p.stdin.write("\n".join(paths)+'\n')
def str_to_ind(s):
try:
if "-" in s:
return slice(*map(int,s.split('-')))
else:
return int(s)
except ValueError:
pass
# must be a substring
return s
def select(paths):
if args.select is '_NOPE_' or not args.select:
sys.stdout.write("Select an option (eg 1,3-7,10 or substring): ")
sys.stdout.flush()
old_stdin = sys.stdin
if not sys.stdin.isatty():
sys.stdin = None
try:
sys.stdin = open('/dev/tty')
except IOError:
pass
if not sys.stdin and sys.stdout.isatty():
sys.stdin = sys.stdout
elif not sys.stdin and sys.stderr.isatty():
sys.stdin = sys.sterr
ranges = sys.stdin.readline().strip()
sys.stdin = old_stdin
print("")
else:
ranges = args.select
ranges=map(str_to_ind, ranges.split(','))
ret=[]
for r in ranges:
try:
if isinstance(r, int):
ret += [paths[r]]
elif isinstance(r, str):
ret += [p for p in paths if r in p]
else:
ret += paths[r]
except IndexError as e:
pass
return ret
files = []
# Seperate positional args into -[0-9]+ and files
# Sadly can't use these number short arguments clumped up with other ones :(
for a in args.files[0]:
if a[0] == '-':
try:
l = int(a[1:])
limit = l
continue
except:
pass
files.append(a)
old_stdout = sys.stdout
if args.select is not "_NOPE_":
# We need to display a list of options and prompt the user for their
# selection. Assume we are interactive and hijack tty. This is for
# the case of eg vim `history 10 | ~/bin/sfs -1 -s`
if not sys.stdout.isatty():
sys.stdout = None
try:
sys.stdout = open('/dev/tty', 'w')
except IOError:
pass
if not sys.stdout and sys.stdin.isatty():
sys.stdout = sys.stdin
elif not sys.stdout and sys.stderr.isatty():
sys.stdout = sys.sterr
paths = []
for line in fileinput.input(files=files):
cmd = shlex.split(line)
if args.exists:
paths += map(os.path.expanduser, filter(lambda x: '/' in x and
os.path.exists(os.path.expanduser(x)), cmd))
else:
paths += filter(lambda x: '/' in x, cmd)
if not args.reverse and limit:
if args.select is '_NOPE_' and args.limit <= len(paths):
if args.reverse: paths=list(reversed(paths))
if not args.select or args.select is "_NOPE_":
display(paths, args.select is not "_NOPE_")
if args.select is not "_NOPE_":
paths=select(paths)
sys.stdout = old_stdout
display(paths)
sys.exit(0)
if args.reverse: paths=list(reversed(paths))
if not args.select or args.select is "_NOPE_":
display(paths, args.select is not "_NOPE_")
if args.select is not "_NOPE_":
paths=select(paths)
sys.stdout = old_stdout
display(paths)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment