Note that as of version 292, xterm removes ASCII control characters except \b
, \r
, \t
, DEL
(0x7f) and \n
(it converts \n
to \r
s like other terminals), and you can bring them back with the allowPasteControls
resource. VTE (the terminal emulator library used by gnome-terminal
, terminator
, xfce-terminal
...) also does it since October 2015
So in those terminals, ^C
, ^[
, ^D
, ^Z
, ^\
, ^U
, ^W
are no longer a problem but DEL, \b
, \t
(very dangerous with some configurations (including the default one) of zsh
where completion can expand command substitutions), \r
and \n
still are.
xterm
also has a couple of paste modes that can help here.
the bracketed paste mode enabled with the \e[?2004h
sequence as used in some zsh or vim safe-paste plugins.
zsh
(zle
) since 5.1 (2015), bash
(readline
) since 4.4 (2016), fish
since 2.6.0 (2017) now have support for that built in. In versions of bash
prior to 5.1 (late 2020), you need to enable it manually with bind 'set enable-bracketed-paste on'
in bash
(or in the readline
configuration in ~/.inputrc
or /etc/inputrc
).
This one wraps the selection between \e[200~
and \e[201~
.
Most other modern terminals like VTE-based ones (gnome-terminal
, xfce-terminal
, terminator
...), rxvt
, konsole
, OS/X Terminal
now also support that one.
In some of those other terminals (or versions thereof) though (or with allowPasteControls
in xterm
), that's flawed in that \e[201~
may appear in the selection, and would be taken as the closing bracket.
That could be fixed by bracketing like \e\e[201~\e[200~201~
, but it's not done by any terminal emulator yet AFAIK (and would mean the application would see several pastes).
^C
/^Z
/^\
would also still cause signals to be sent to the foreground process group of the terminal if ISIG
was not disabled in the tty line discipline.
The quoted paste mode enabled with the \e[?2005h
sequence (disabled with \e[?2005l
).
This one prepends every character (actually byte) with a ^V
character.
^V
is the default lnext
(literal next) character of the tty line discipline in canonical mode, and is also recognised as such by vi
and other editors and some line editors like readline or zsh
's zle.
That one doesn't have the same problem as the bracketed mode above, and has the benefit to work for the terminal canonical mode (like when you do cat > file
) and a few other applications but has a few drawbacks:
- newline and CR end up being rendered as
^M
. That can be avoided with another escape sequence: \e[?2006h
, but that causes the newlines to be inserted as NUL characters in vim
and show up as ^J
(unless you do stty -echoctl
) in the terminal canonical mode (though it's only a cosmetic issue).
- That doesn't work great for multi-byte characters which are not inserted properly in
zle
or vim
for instance.
- some visual applications don't handle
^V
as literal next, so you may still have to turn it off selectively.
- you can't use it in
vim
as ^V 1
for instance doesn't insert 1
but ^A
there.
- I'm not aware of any other terminal beside
xterm
supporting it, but then I've not done an extensive survey.
It also lets you define your own bracketed paste mode via configuration. For instance, with:
XTerm*allowPasteControls: true
XTerm.VT100.translations: #override \
Ctrl Shift<KeyPress> Insert: \
insert-formatted("\033[202~%S~%s", CLIPBOARD,PRIMARY,CUT_BUFFER0)'
it would insert the CLIPBOARD
/PRIMARY
/CUT_BUFFER0
as ^[[202~<size-in-bytes>~<content>
upon Shift+Ctrl+Insert. The application could then interpret that reliably (it would still need to disable ISIG
in the tty line discipline though).
Another approach would be to use a pseudo-tty wrapper that inserts those ^V
only in front of control characters. Such wrapper should be able to detect control characters in pastes with some reliability because real keyboard keypresses would only send one character at a time or a sequence of characters starting with ESC, while pastes would send several at a time.
You'd still have the problem of newlines shown as ^J
in the terminal canonical mode or ^@
in vim
, but that could be worked around with with some cooperation by the shell
A proof of concept:
To be used for instance as:
./safe-paste bash
To start a bash
shell under that wrapper.
#!/usr/bin/perl
use IO::Pty;
use IO::Stty;
my $pty = new IO::Pty;
my $pid = fork();
die "Cannot fork" if not defined $pid;
unless ($pid) {
$pty->make_slave_controlling_terminal();
my $slave = $pty->slave();
close $pty;
$slave->clone_winsize_from(\*STDIN);
open(STDIN,"<&". $slave->fileno())
or die "Couldn't reopen STDIN for reading, $!\n";
open(STDOUT,">&". $slave->fileno())
or die "Couldn't reopen STDOUT for writing, $!\n";
open(STDERR,">&". $slave->fileno())
or die "Couldn't reopen STDERR for writing, $!\n";
close $slave;
exec(@ARGV);
die "Cannot exec(@ARGV): $!";
}
$pty->close_slave();
$SIG{WINCH} = sub {
$pty->slave->clone_winsize_from(\*STDIN);
};
my $old = IO::Stty::stty(\*STDIN, '-g');
IO::Stty::stty(\*STDIN, 'raw', '-echo');
$tty = fileno($pty);
my ($rin,$ein) = ('','','');
vec($rin, 0, 1) = 1;
vec($rin, $tty, 1) = 1;
vec($ein, $tty, 1) = 1;
my ($to_stdout, $to_tty) = ('', '');
my $eof;
$SIG{CHLD} = sub {$eof = 1};
until ($eof && $to_stdout eq '' && $to_tty eq '') {
my ($rout,$wout,$eout,$timeleft);
my $win = '';
vec($win, 0, 1) = 1 if ($to_stdout ne "");
vec($win, $tty, 1) = 1 if ($to_tty ne "");
($nfound,$timeleft) = select($rout=$rin,$wout=$win,$eout=$ein,undef);
if ($nfound > 0) {
if (vec($eout, $tty, 1)) {
print STDERR "Exception on $tty\n";
}
if (vec($rout, 0, 1)) {
my $buf;
if (sysread(STDIN, $buf, 4096)) {
if ($buf =~ /.[\0-\037\177]/ || $buf =~ /^(?:[\0-\032\034-\037]|\033.*?[~a-zA-NP-Z])./) {
$buf =~ s/[\0-\037\177]/\026$&/g;
# TODO: add UTF-8 sanitizing
$buf =~ y/\r/\n/;
}
$to_tty .= $buf;
} else {
$eof = 1;
vec($rin, 0, 1) = 0;
}
}
if (vec($rout, $tty, 1)) {
my $buf;
if (sysread($pty, $buf, 4096)) {
$to_stdout .= $buf;
} else {
$eof = 1;
vec($rin, $tty, 1) = 0;
$to_tty = '';
}
}
if ($to_tty ne '' && vec($wout, $tty, 1)) {
my $written = syswrite($pty, $to_tty);
$to_tty = substr($to_tty, $written) if $written;
}
if ($to_stdout ne '' && vec(wout, 1, 1)) {
my $written = syswrite(STDOUT, $to_stdout);
$to_stdout = substr($to_stdout, $written) if $written;
}
}
}
END{IO::Stty::stty(\*STDIN, $old)}
A better approach would probably be to use a clipboard manager where you can specify the paste mode and that would flag potentially dangerous selections.