How to trick an application into thinking its stdout is a terminal, not a pipe
Asked Answered
B

10

204

I'm trying to do the opposite of "Detect if stdin is a terminal or pipe?".

I'm running an application that's changing its output format because it detects a pipe on STDOUT, and I want it to think that it's an interactive terminal so that I get the same output when redirecting.

I was thinking that wrapping it in an expect script or using a proc_open() in PHP would do it, but it doesn't.

Any ideas out there?

Barber answered 9/9, 2009 at 17:31 Comment(4)
Does empty.sf.net help?Curvy
@Curvy : should have been an answer. Great util by the way ...Revealment
The question talks about stdout but the title mentions stdin. I think the title is wrong.Pelagic
What about the same for stdin?Unrefined
B
210

Aha!

The script command does what we want...

script --return --quiet -c "[executable string]" /dev/null

Does the trick!

Usage:
 script [options] [file]

Make a typescript of a terminal session.

Options:
 -a, --append                  append the output
 -c, --command <command>       run command rather than interactive shell
 -e, --return                  return exit code of the child process
 -f, --flush                   run flush after each write
     --force                   use output file even when it is a link
 -q, --quiet                   be quiet
 -t[<file>], --timing[=<file>] output timing data to stderr or to FILE
 -h, --help                    display this help
 -V, --version                 display version
Barber answered 9/9, 2009 at 22:6 Comment(15)
+1: just stumble on the problem with a lib that do static initialisation. A recent change in Fedora 12 has make that init fail when the exe lanched was not in a tty. Your trick works perfectly. I preferred it over unbuffer as script is installed by default !Revealment
script is even available in BusyBox!Responsion
If you want to pipe it into something interactive, like less -R, where terminal input goes to less -R, then you need some extra trickery. For example, I wanted a colourful version of git status | less. You need to pass -R to less in order that it respect the colours, and you need to use script to get git status to output colour. But we don't want script to keep ownership of the keyboard, we want this to go to less. So I use this now and it works well: 0<&- script -qfc "git status" /dev/null | less -R . Those first few characters close stdin for this one commmand.Loup
Note: This doesn't work in cases where the component checking for interactivity is looking at the $- shell variable for an "i".Genu
Beware: you may want to use the --return (-r) option for script, if it is available, so the return code of the child process will be preserved. Otherwise, even if the command fails, your script command will happily return a success code.Beckett
@AaronMcDaid Better use 0</dev/null instead of closing STDIN. Otherwise script will complain with :script: write error.Revoice
{ 0</dev/null script -qfc "g++ x.cpp" /dev/null; } | less -R Thanks for the suggestion, @Kontrollfreak. This is what I use know to view, in less, the colourful output of something that doesn't take input.Loup
This doesn't work for me, it's somehow still reading the real TTY.Hooded
This is amazing. I needed this for an extremely rare use case with an embedded Python library within an executable that is run within Wine. When I ran in a terminal it worked but when I ran the .desktop file I created it would always crash because Py_Initialize didn't see proper stdin/stderr.Medico
Is there an OS agnostic way of doing this? I like the script command on macOS because you don't have to wrap the command in quotes. The script runs and sends output to the tty which is duplicated in the supplied file, but I can't seem to get the linux version to behave the same way... I'm probably doing something wrong. So what's the equivalent linux script command for this on macOS: script -q -t 0 tmp.out perl -e 'print "Test\n"' Test cat tmp.out TestFrankly
@Beckett the equivalent for --return is -e, not -r. (According to the man for script from util-linux 2.31.1)Liesa
@AaronMcDaid unbuffer works with piping to less. That may be an easier syntax than what you've got. stackoverflow.com/a/1410273Liesa
@Frankly the os agnostic way to do this on Mac/BSD and Linux/GNU is given in the other answer: https://mcmap.net/q/46590/-how-to-trick-an-application-into-thinking-its-stdout-is-a-terminal-not-a-pipeAccusative
Why isn't that marked as correct?Biennial
What about on systems that running script returns bash: script: command not found? This evidently does not come installed with Git Bash for Windows. Googling variations on "script command not found" does not actually have any results about the script command but instead random bash script commands.Wilinski
H
83

Based on Chris' solution, I came up with the following little helper function:

faketty() {
    script -qfc "$(printf "%q " "$@")" /dev/null
}

The quirky looking printf is necessary to correctly expand the script's arguments in $@ while protecting possibly quoted parts of the command (see example below).

Usage:

faketty <command> <args>

Example:

$ python -c "import sys; print(sys.stdout.isatty())"
True
$ python -c "import sys; print(sys.stdout.isatty())" | cat
False
$ faketty python -c "import sys; print(sys.stdout.isatty())" | cat
True
Horan answered 5/12, 2013 at 13:47 Comment(3)
You probably want to use the --return option, if your version of script has it, to preserve the child process' exit code.Beckett
I recommend changing this function like so: function faketty { script -qfc "$(printf "%q " "$@")" /dev/null; } Otherwise, a file named typescript will be created each time a command is run, in many cases.Mitigate
doesn't seem to work on MacOS tho, I get script: illegal option -- fOneiromancy
E
33

The unbuffer script that comes with Expect should handle this ok. If not, the application may be looking at something other than what its output is connected to, eg. what the TERM environment variable is set to.

Expel answered 11/9, 2009 at 11:3 Comment(0)
C
23

Referring previous answer, on Mac OS X, "script" can be used like below...

script -q /dev/null commands...

But, because it may replace "\n" with "\r\n" on the stdout, you may also need script like this:

script -q /dev/null commands... | perl -pe 's/\r\n/\n/g'

If there are some pipe between these commands, you need to flush stdout. for example:

script -q /dev/null commands... | ruby -ne 'print "....\n";STDOUT.flush' |  perl -pe 's/\r\n/\n/g'
Calif answered 27/11, 2012 at 15:51 Comment(1)
Thanks for the OS X syntax, but, judging by your Perl statement, it seems that you meant to say that it changes instances of "\r\n" to "\n", not the other way around, correct?Cates
C
19

I don't know if it's doable from PHP, but if you really need the child process to see a TTY, you can create a PTY.

In C:

#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>
#include <unistd.h>
#include <pty.h>

int main(int argc, char **argv) {
    int master;
    struct winsize win = {
        .ws_col = 80, .ws_row = 24,
        .ws_xpixel = 480, .ws_ypixel = 192,
    };
    pid_t child;

    if (argc < 2) {
        printf("Usage: %s cmd [args...]\n", argv[0]);
        exit(EX_USAGE);
    }

    child = forkpty(&master, NULL, NULL, &win);
    if (child == -1) {
        perror("forkpty failed");
        exit(EX_OSERR);
    }
    if (child == 0) {
        execvp(argv[1], argv + 1);
        perror("exec failed");
        exit(EX_OSERR);
    }

    /* now the child is attached to a real pseudo-TTY instead of a pipe,
     * while the parent can use "master" much like a normal pipe */
}

I was actually under the impression that expect itself does creates a PTY, though.

Curvy answered 9/9, 2009 at 19:21 Comment(1)
Do you know how to run nettop as the child process on mac os x? I want to get nettop's output in my app. I tried using forkpty but still could not run nettop successfully.Bourbon
V
17

Updating @A-Ron's answer to a) work on both Linux & MacOs b) propagate status code indirectly (since MacOs script does not support it)

faketty () {
  # Create a temporary file for storing the status code
  tmp=$(mktemp)

  # Ensure it worked or fail with status 99
  [ "$tmp" ] || return 99

  # Produce a script that runs the command provided to faketty as
  # arguments and stores the status code in the temporary file
  cmd="$(printf '%q ' "$@")"'; echo $? > '$tmp

  # Run the script through /bin/sh with fake tty
  if [ "$(uname)" = "Darwin" ]; then
    # MacOS
    script -Fq /dev/null /bin/sh -c "$cmd"
  else
    script -qfc "/bin/sh -c $(printf "%q " "$cmd")" /dev/null
  fi

  # Ensure that the status code was written to the temporary file or
  # fail with status 99
  [ -s $tmp ] || return 99

  # Collect the status code from the temporary file
  err=$(cat $tmp)

  # Remove the temporary file
  rm -f $tmp

  # Return the status code
  return $err
}

Examples:

$ faketty false ; echo $?
1

$ faketty echo '$HOME' ; echo $?
$HOME
0

embedded_example () {
  faketty perl -e 'sleep(5); print "Hello  world\n"; exit(3);' > LOGFILE 2>&1 </dev/null &
  pid=$!

  # do something else
  echo 0..
  sleep 2
  echo 2..

  echo wait
  wait $pid
  status=$?
  cat LOGFILE
  echo Exit status: $status
}

$ embedded_example
0..
2..
wait
Hello  world
Exit status: 3
Volution answered 18/2, 2020 at 11:4 Comment(3)
I use a nonstandard SHELL so this function was failing in my (Linux) environment. The script command uses SHELL to run /bin/sh -c ... which runs $cmd. By explicitly setting SHELL=/bin/sh it seems possible to cut out one of those three stages. I replaced the Linux script line with the simpler SHELL=/bin/sh script -qfc "$cmd" /dev/null, and it appears to function identically.Brookweed
on Mac: Your script doesn't seem to take care of stdin, does it? E.g. uniq doesn't seem to work when it's wrapped in faketty while getting its stdin from a pipe: grep --line-buffered ... | faketty uniq. (I need faketty because I require to pipe uniq's output to yet another command without buffering.)Boyes
Unfortunately I don't have a Mac (a friend helped me out to create the Mac part) and can't seem to find the manual page easily.Volution
C
9

Too new to comment on the specific answer, but I thought I'd followup on the faketty function posted by ingomueller-net above since it recently helped me out.

I found that this was creating a typescript file that I didn't want/need so I added /dev/null as the script target file:

function faketty { script -qfc "$(printf "%q " "$@")" /dev/null ; }

Cinerarium answered 29/1, 2017 at 8:39 Comment(0)
U
3

There's also a pty program included in the sample code of the book "Advanced Programming in the UNIX Environment, Second Edition"!

Here's how to compile pty on Mac OS X:

man 4 pty  #  pty -- pseudo terminal driver

open http://en.wikipedia.org/wiki/Pseudo_terminal

# Advanced Programming in the UNIX Environment, Second Edition
open http://www.apuebook.com

cd ~/Desktop

curl -L -O http://www.apuebook.com/src.tar.gz

tar -xzf src.tar.gz

cd apue.2e

wkdir="${HOME}/Desktop/apue.2e"

sed -E -i "" "s|^WKDIR=.*|WKDIR=${wkdir}|" ~/Desktop/apue.2e/Make.defines.macos

echo '#undef _POSIX_C_SOURCE' >> ~/Desktop/apue.2e/include/apue.h

str='#include   <sys/select.h>'
printf '%s\n' H 1i "$str" . wq | ed -s calld/loop.c

str='
#undef _POSIX_C_SOURCE
#include <sys/types.h>
'
printf '%s\n' H 1i "$str" . wq | ed -s file/devrdev.c

str='
#include <sys/signal.h>
#include <sys/ioctl.h>
'
printf '%s\n' H 1i "$str" . wq | ed -s termios/winch.c

make

~/Desktop/apue.2e/pty/pty ls -ld *
Ucayali answered 5/8, 2010 at 10:40 Comment(1)
Really weird error, too: Fastly error: unknown domain: codesnippets.joyent.com. Please check that this domain has been added to a service.Kittenish
G
3

I was trying to get colors when running shellcheck <file> | less on Linux, so I tried the above answers, but they produce this bizarre effect where text is horizontally offset from where it should be:

In ./all/update.sh line 6:
                          for repo in $(cat repos); do
                                                                  ^-- SC2013: To read lines rather than words, pipe/redirect to a 'while read' loop.

(For those unfamiliar with shellcheck, the line with the warning is supposed to line up with the where the problem is.)

In order to the answers above to work with shellcheck, I tried one of the options from the comments:

faketty() {                       
    0</dev/null script -qfc "$(printf "%q " "$@")" /dev/null
}

This works. I also added --return and used long options, to make this command a little less inscrutable:

faketty() {                       
    0</dev/null script --quiet --flush --return --command "$(printf "%q " "$@")" /dev/null
}

Works in Bash and Zsh.

Godown answered 6/8, 2019 at 17:14 Comment(3)
quite sure that does not work in neither bash nor zsh on bsd systems.Accusative
@Accusative I encourage you to submit an answer that does, then.Godown
No need, another answer covers that. I was just saying that the main driver of your answer is OS dependant, regardless of the shell. I can make an edit instead, to clarify :)Accusative
O
-2

Just in case you are looking for color highlight Ansible output with tee you also my use

export ANSIBLE_FORCE_COLOR=True
Orvalorvan answered 27/2, 2023 at 16:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.