0

In an interactive shell script I'd like to save the output of a command to a file like in

$ echo "Hello, World" > test.txt

but prevent test.txt to be overwritten if it already exists. So I thought it would be a good idea to use cp with it's interactive option (cp -i) to check if the target file exists. I tried this:

$ echo -n "Hello" | cp -i /dev/stdin test.txt 

This writes "Hello" to test.txt if test.txt doesn't exist yet, but aborts copying if test.txt exists, as cp reads the answer to whether to overwrite or not from just that pipe.

However, this

$ cp -i <(echo "World") test.txt
cp: overwrite 'test.txt'? y
'/proc/self/fd/11' -> 'test.txt'

works as intended, cp seems to take the subprocess' filedesriptor as source and this is cp'ied to test.txt and I simply don't know why.

Any ideas or explanations or better ways to do it?

4
  • I don't get it. What are you expecting/hoping it will do? Why are you using the interactive option? You say "to prevent overwriting". You then say this fails, because it does not overwrite the existing file "But this fails when test.txt already exists as cp quits with asking and NOT copying". You then say something that dose not make sense about newline characters "as there was a newline in the echoed line anyway". Apr 6, 2020 at 21:45
  • Is your question: "Why am I not prompted when I .....?" Apr 6, 2020 at 21:46
  • Basically, yes, that's my question. and oh, I was just blind to the fact that piping anything with 'y' to stdin was simply read by cp as an answer to the -i guard. But I still don't get why @uncle-billy 's solution works without a subshell (like mine) but by grouping a list...
    – zot
    Apr 6, 2020 at 22:03
  • You can edit your question, to make it clearer. And I don't get which part of uncle bill's answer you don't grok. Apr 6, 2020 at 22:20

2 Answers 2

0

Use another file descriptor instead of /dev/stdin

echo Hello | { cp -i /dev/fd/5 test.txt 5<&0 </dev/tty; }

Or, still scriptable via yes(1)

{ echo Hello | { cp -i /dev/fd/5 test.txt 5<&0 <&4; }; } 4<&0

In both variants, the extra { ... } around cp -i ... are only needed in zsh, in order to work around its non-standard multios feature.

This should work in any standard shell and any system which supports /dev/fd/N files, except for the Linux + ksh93 combination, which won't work because ksh93 is implementing pipes with Unix sockets, which on Linux cannot be opened via /dev/fd/N (not even echo | cat /dev/stdin will work).

Your echo -n "Hello" | cp -i /dev/stdin test.txt will actually clobber the file if instead of "Hello" you put "You suck!" there. That's one of the reasons why you shouldn't rely on cp -i.

9
  • Thanks, but the former doesn't work at all for me, even if test.txt doesn't exist, it seems to be touched, but left empty.
    – zot
    Apr 6, 2020 at 20:49
  • Maybe your cp is an alias. The fact that it shows the files being copied (as cp -v) suggests as much.
    – user313992
    Apr 6, 2020 at 20:53
  • Also, where did you get the idea that it needs a newline: it doesn't. printf y | cp -vi src dst will clobber dst, without any newline after y.
    – user313992
    Apr 6, 2020 at 21:02
  • It acutally is, but echo Hello | /bin/cp -i /dev/fd/5 test.txt 5<&0 </dev/tty results in the same empty file. And I don't get why replacing the echo'ed text would clobber the file? Isn't cp -i meant to prevent just that?
    – zot
    Apr 6, 2020 at 21:02
  • It does not, on my system. What system are you using? cp -i reads a line from stdin (it doesn't have to be a complete line, terminated by \n, it may be all the text up to EOF), and if it starts by y or Y goes on and clobbers the file.
    – user313992
    Apr 6, 2020 at 21:06
0

Shells such as bash can already protect against overwriting files with a redirect

set -o noclobber
echo hello >world
echo hello >world
-bash: world: cannot overwrite existing file

If you want to be able to ask the user before overwriting, I would use something like this instead of the noclobber setting

#!/bin/bash

# Define function sureYN  [<prompt>]  <target>
sureYN() {
    local prompt="$1" target="$2" yn='y'
    [[ -z "$target" ]] && { target="$prompt"; prompt="Overwrite $target"; }

    [[ -f "$target" && -t 0 ]] && read -p "$prompt (y/n)? " yn >&2
    [[ "$yn" =~ ^[Yy] ]]
}

sureYN /tmp/first && cp -fp /etc/hosts /tmp/first
sureYN "Can I replace /tmp/second" /tmp/second && echo this is the second >/tmp/second
4
  • Thx, that is most useful but later I need to let the user decide whether to overwrite or not. So uncle billy's approach might be more suitable once I understand that maze of curly brackets...
    – zot
    Apr 7, 2020 at 0:00
  • If it's a script what's wrong with yn=y; [[ -f "$target" ]] && read -p "Are you sure? " yn; [[ "$yn" == y ]] && echo foo >"$target" or whatever?
    – roaima
    Apr 7, 2020 at 0:06
  • technically nothing except for my wish to reuse existing tools (if it's possible) and for that I would have to use a named pipe if the "echo foo" part becomes a real piped stream, or am I missing something?
    – zot
    Apr 7, 2020 at 0:16
  • @zot there you go: a reusable function for your script
    – roaima
    Apr 7, 2020 at 9:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.