Piping multiple values into a program in a batch script
Asked Answered
W

3

5

I'm writing my own simple system allowing me to automatically sign APKs before they are uploaded to GPlay. I've got a batch file that does the signing; and a "wrapper" batch file, the content of which will be run on the command line by Jenkins post-build.

sign_apks.bat:

@echo off

set /p job= "Enter job name: "
set /p alias= "Enter key alias: "
set /p mobile= "Sign mobile? (y/n): "
set /p wear= "Sign wear? (y/n): "

echo.
echo "%job%"
echo "%alias%"
echo "%mobile%"
echo "%wear%"

[the rest of the code is sensitive so is emitted, but these variables are used later]

wrapper code:

@echo off

(echo test
echo test
echo y
echo y)| call sign_apks.bat

This article showed me how to pipe values into a program. To quote from the answer,:

Multiple lines can be entered like so:

(echo y
echo n) | executable.exe

...which will pass first 'y' then 'n'.

However, this doesn't work. This is the output I get when running the wrapper code:

Enter job name: Enter key alias: Sign mobile? (y/n): Sign wear? (y/n):
"test "
""
""
""

Help?

Whitfield answered 23/12, 2014 at 14:36 Comment(0)
B
1

You know, the easiest solution would be to supply job, alias, mobile, and wear as script arguments rather than trying to pipe them into stdin. You can still set /p if not defined, if you wish to run interactively without arguments.

@echo off
setlocal

set "job=%~1"
set "alias=%~2"
set "mobile=%~3"
set "wear=%~4"

if not defined job set /p "job=Enter job name: "
if not defined alias set /p "alias=Enter key alias: "
if not defined mobile set /p "mobile=Sign mobile? (y/n): "
if not defined wear set /p "wear=Sign wear? (y/n): "

echo.
echo "%job%"
echo "%alias%"
echo "%mobile%"
echo "%wear%"

Then when you call sign_apks.bat, just call it like this:

call sign_apks.bat test test y y
Bremerhaven answered 23/12, 2014 at 15:25 Comment(1)
Hadn't thought of that. Thanks very much :)Whitfield
M
5

There is something very odd with how SET /P interacts with your piped input that I do not fully understand.

But I do have some solutions :-)

The simplest solution is to write your responses to a temporary file, and then use that temp file as redirected input.

@echo off
(
  echo test
  echo test
  echo y
  echo y
)>responses.temp
call sign_apks.bat <responses.temp
delete responses.temp

That is how I would solve your problem. But some people do not like to use temporary files (why I don't know). So I decided I would attempt to solve it using a pipe without a temp file.

I discovered an odd variation of your code that almost solves the problem - but it appends an extra unwanted space at the end of each value.

@echo off
(
  call echo test1
  call echo test2
  call echo y1
  call echo y2
) | sign_apks.bat

--OUTPUT--

Enter job name: Enter key alias: Sign mobile? (y/n): Sign wear? (y/n):
"test1 "
"test2 "
"y1 "
"y2 "

I cannot explain why the CALL enables each of the SET /P statements to work properly. But I can explain why the space is appended to each value. It has to do with why CALL is not needed when you use a batch script with a pipe.

Each side of a pipe is executed in a brand new cmd.exe session. For example, the right side of the pipe becomes a command that looks something like:

C:\Windows\system32\cmd.exe /S /D /c" sign_apks.bat"

This is the reason why CALL is not needed - control will return after the new cmd.exe session terminates.

The unwanted spaces are an artifact of how pipes process parenthesized blocks. The parser must capture the entire piped code block and transform it into a single line that can be incorporated into the CMD.EXE /C argument. The CMD.EXE parser does this by putting an & between each command. Unfortunately, the parser also inserts some extra spaces. So the left side of the pipe is transformed into something like:

C:\Windows\system32\cmd.exe /S /D /c" ( call echo test & call echo test & call echo y & call echo y )"

Now you can easily see where the unwanted trailing spaces are coming from. See Why does delayed expansion fail when inside a piped block of code? for more information about how pipes are implemented.

I finally came up with one more solution. I created a helper batch script called WriteArgs.bat that simply ECHOs each argument passed to it.

WriteArgs.bat

@echo off
:loop
if .%1 equ . exit /b
echo %1
shift /1
goto loop

With this simple batch script, you can now solve your problem using:

WriteArgs.bat test test y y | sign_apks.bat

Again, I don't understand why SET /P works properly here, yet doesn't work with your original command. But this does solve the problem :-)

Update - Well, it solves the problem on my machine. But it seems to be a timing issue, and I don't have confidence that any given piped solution will always work. The only solution I feel is robust is the one that uses a temp file and redirection.

Michele answered 23/12, 2014 at 16:38 Comment(5)
The cause why CALL echo works is simple, it's slower. That's also the cause why echo doesn't work with SET /p. But you find nice work aroundsNovobiocin
@Novobiocin - Good point, CALL certainly does slow the left side down. At first I thought this was a timing issue. But I still can't wrap my head around it. I can't quite explain all the observed behavior. If the left side is faster than the right, then why doesn't SET /P stop at the first newline so that the next SET /P can read the next line? Without CALL it is behaving as if the first SET /P is reading all lines of input, but only storing the first one.Michele
I will start a thread on dostips for this, I played with this issues for a whileNovobiocin
@Novobiocin - I look forward to your explanation. The SET /P behavior irritates me.Michele
The thread discussing the strange behaviour is at set /p problems with pipesNovobiocin
B
1

You know, the easiest solution would be to supply job, alias, mobile, and wear as script arguments rather than trying to pipe them into stdin. You can still set /p if not defined, if you wish to run interactively without arguments.

@echo off
setlocal

set "job=%~1"
set "alias=%~2"
set "mobile=%~3"
set "wear=%~4"

if not defined job set /p "job=Enter job name: "
if not defined alias set /p "alias=Enter key alias: "
if not defined mobile set /p "mobile=Sign mobile? (y/n): "
if not defined wear set /p "wear=Sign wear? (y/n): "

echo.
echo "%job%"
echo "%alias%"
echo "%mobile%"
echo "%wear%"

Then when you call sign_apks.bat, just call it like this:

call sign_apks.bat test test y y
Bremerhaven answered 23/12, 2014 at 15:25 Comment(1)
Hadn't thought of that. Thanks very much :)Whitfield
N
1

It's a problem of set /p, it reads the input buffer, but it fails to split this buffer when multiple lines are available, it simply takes the first line from the buffer and the rest will be discarded.

This isn't a problem for a single echo piped to a single set/p, but when you pipe more lines to multiple set/p you got random results.

The solution of dbenham can work, but it's depends on your system!
As both processes (line producer and the set/p consumer) are asnchronously running in an own cmd.exe task, it depends on the cpu time each process gets.

But you can ensure a correct consuming by splitting the content by another program like more or findstr.
As these split the input buffer proberly at the line boundarys.

Novobiocin answered 23/12, 2014 at 17:11 Comment(1)
But SET /P has no problem with multiple lines of redirected input. That implies to me that the method by which SET /P reads piped input must be quite different than how it reads redirected input. That just seems weird.Michele

© 2022 - 2024 — McMap. All rights reserved.