Why does `runScript =` in `buildFHSUserEnv` stop after first line when using two single quotes? - nix

Running the below shell.nix using nix-shell ./shell.nix gives the expected output:
{ pkgs ? import <nixpkgs> {} }:
(pkgs.buildFHSUserEnv {
name = "test";
runScript = "
echo one
echo two
";
}).env
one
two
as expected.
But changing the double quotes to two single quotes only seems to execute the first line of runScript:
{ pkgs ? import <nixpkgs> {} }:
(pkgs.buildFHSUserEnv {
name = "test";
runScript = ''
echo one
echo two
'';
}).env
one
This is strange, since both quoting styles do evaluate to multi-line strings with all the lines I entered:
$ nix-instantiate --eval -
"
first line
second line
"
"\n first line\n second line\n"
$ nix-instantiate --eval -
''
first line
second line
''
"first line\nsecond line\n"

runScript isn't treated as a proper bash script, but is rather pasted directly after the beginning of an exec statement.
The generated code becomes either
exec
echo one
echo two
or
exec echo one
echo two
because '' removes the starting newline.
exec by itself is a no-op in bash, so that happens to behave as expected, but exec echo one replaces the shell interpreter by an echo one process, which is not what you intended.
Seems like the runScript parameter should be deprecated and replaced by two optional ones which are self-explanatory, unless this behavior is caused by a recent change, which would make it a regression (bug).

Related

How to refer to custom derivation in shell.nix?

I'm very new to Nix. I'd like to refer to project scripts in my shell.nix file, so that when I cd into my project directory I can refer to them by name, and I can keep them up-to-date whenever the sources change.
To learn how to do this, I created a very simple derivation for a shell script. Eventually I'd like to use other languages, but I'm starting simple. It looks like this:
project
nix
myScript
default.nix
builder.sh
shell.nix
# default.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation rec {
name = "myScript";
echo = pkgs.coreutils + "/bin/echo";
builder = "${pkgs.coreutils}/bin/bash";
args = [ ./builder.sh ];
}
# builder.sh
$echo "$echo Hello world" > $out
When I run nix-build myScript.nix it creates a symlinked result file that looks like this:
/nix/store/3mfkgajns47hfv0diihzi2scwl4hm2fl-coreutils-9.1/bin/echo Hello world
I tried referencing this in my shell.nix file like this:
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/bf972dc380f36a3bf83db052380e55f0eaa7dcb6.tar.gz") {} }:
let
myScript = import ./myScript {};
in
pkgs.mkShell {
buildInputs = [
myScript
];
shellHook = ''
echo Loading shell.nix
'';
}
But whenever I enter the projects directory and run the command `myScript, I get an error:
zsh: command not found: myScript
I already have direnv configured correctly, which I can confirm by adding other shell tools and checking their versions. So it's something wrong with my nix files.
I'm almost certainly doing something wrong here. I know I can simplify this with pkgs.writeShellScriptBin, but the shell script is more a minimal example of what I want to get working. Eventually I'd use more complex derivations.
What I think is wrong
I think the myScript derivation or builder is doing something wrong. It does create the expected output file (i.e. I can chmod +x and run it, and it works) but I suspect I need to tell nix how to run it? I'm not sure. And also I might be importing the derivation incorrectly.
This is a problem with your default.nix, not your shell.nix.
For mkShell to work with buildInputs as you intend, you need $out to be a directory with an $out/bin/myScript, not a file on its own. nixpkgs has a helper that will do this for you, in https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/trivial-builders.nix --
# default.nix; no builder.sh needed
{ pkgs ? import <nixpkgs> {} }:
pkgs.writeShellScript "myScript" ''
echo "Hello world" # use the bash-builtin echo, not the external coreutils one
'';

How to setup completion of environment variable names in zsh?

In bash when I try to use autocompletion in double-quoted strings it works:
E.g.
echo "My home directory is $HO<t>"
# expands to
echo "My home directory is $HOME"
But when I try the same thing in zsh it just does nothing.
At the same time it works outside strings:
echo My\ home\ directory\ is\ $HO<t>
# expands to
echo My\ home\ directory\ is\ $HOME
Is it possible to make it work the same as bash?
Zsh completion has a bug where completion of parameter names fails if a partial parameter name is followed by a ".
As a workaround, just remove the final " from your string before pressing Tab. Then, Zsh's completion will work correctly and it will automatically add the final " for you.

Tcl script started from jenkins turns the command to lowercase

I'm having a strange issue when running a tcl command in jenkins.
The tcl script has the following lines (pay attention to the uppercase I in Id):
foreach name $docker_names {
set name "TestName"
puts $name
set command "docker inspect --format='{{.Id}}' ${name} > /home/temp/id.txt"
send -- "$command\n"
expect "$"
}
In the jeknins log I see that the job fails because what is sent in the second iteration of the loop is the command above but in lowercase. I need the I in Id to be uppercase.
This is what is sent in the second iteration of the loop:
docker inspect --format='{{.id}}' testname > /home/temp/id.txt
NOTE: In the first iteration, everything is sent correctly.
Anyone has an idea of why this is happening?
Thanks!
It can't happen from pure TCL perspective. It's a side effect of TCL interpreter integration in Jenkins: I guess there's a hidden layer that parses the script and modify it according to non-TCL syntax.
I would do 2 trials:
First is to minimize the TCL parsings f the string:
foreach name $docker_names {
set name "TestName"
puts $name
set command [concat {docker inspect --format='{{.Id}}'} $name { > /home/temp/id.txt} "\n"]
send -- $command
expect "$"
}
If it doesn't help, try backquoting the curlybraces that may be interpreted by another layer:
foreach name $docker_names {
set name "TestName"
puts $name
set command "docker inspect --format='\{\{.Id\}\}' ${name} > /home/temp/id.txt"
send -- "$command\n"
expect "$"
}

Inserting a matched string from previous line to the current line using sed or awk

I have a CSV file that shows the statistics for links on a half an hour basis. The link name only appears on the 00:00 line.
link1,0:00,0,0,0,0
,00:30,0,0,0,0
,01:00,0,0,0,0
,01:30,0,0,0,0
,02:00,0,0,0,0
,02:30,0,0,0,0
,03:00,0,0,0,0
,03:30,0,0,0,0
,23:30,0,0,0,0
....
....
link2,00:00,0,0,0,0
How do I copy the link name to every other line until the link name is different, using sed or awk?
With awk, just keep track of the last seen non-empty link name, and always use that.
awk -F, -v OFS=, '$1 != "" { link=$1 } { $1 = link; print $0 }'
Omitting the ellipses, this gives:
link1,0:00,0,0,0,0
link1,00:30,0,0,0,0
link1,01:00,0,0,0,0
link1,01:30,0,0,0,0
link1,02:00,0,0,0,0
link1,02:30,0,0,0,0
link1,03:00,0,0,0,0
link1,03:30,0,0,0,0
link1,23:30,0,0,0,0
link2,00:00,0,0,0,0
This is a simpler job with awk, but if you want to use sed:
sed -e '/^[^,]/{h;s/,.*//;x};/^,/{G;s/^\(.*\)\n\(.*\)/\2\1/}'
Bellow a commented version in sed script file format that can be run with sed -f script:
# For lines not beginning with a ',', saves what precedes a ',' in the hold space and print the original line.
/^[^,]/{
h
s/,.*//
x}
# For lines beginning with a ',', put what has been save in the hold space at the beginning of the pattern space and print.
/^,/{
G
s/^\(.*\)\n\(.*\)/\2\1/}
You can do that in pure bash shell without needing to start a new process, which should be faster than using awk or sed:
IFS=","
while read v1 v2; do
if [[ $v1 != "" ]]; then
link=$v1;
fi
printf "%s,%s\n" "$link" "$v2"
done < file

How can I tell from a within a shell script if the shell that invoked it is an interactive shell?

I'm trying to set up a shell script that will start a screen session (or rejoin an existing one) only if it is invoked from an interactive shell. The solution I have seen is to check if $- contains the letter "i":
#!/bin/sh -e
echo "Testing interactivity..."
echo 'Current value of $- = '"$-"
if [ `echo \$- | grep -qs i` ]; then
echo interactive;
else
echo noninteractive;
fi
However, this fails, because the script is run by a new noninteractive shell, invoked as a result of the #!/bin/sh at the top. If I source the script instead of running it, it works as desired, but that's an ugly hack. I'd rather have it work when I run it.
So how can I test for interactivity within a script?
Give this a try and see if it does what you're looking for:
#!/bin/sh
if [ $_ != $0 ]
then
echo interactive;
else
echo noninteractive;
fi
The underscore ($_) expands to the absolute pathname used to invoke the script. The zero ($0) expands to the name of the script. If they're different then the script was invoked from an interactive shell. In Bash, subsequent expansion of $_ gives the expanded argument to the previous command (it might be a good idea to save the value of $_ in another variable in order to preserve it).
From man bash:
0 Expands to the name of the shell or shell script. This is set
at shell initialization. If bash is invoked with a file of com‐
mands, $0 is set to the name of that file. If bash is started
with the -c option, then $0 is set to the first argument after
the string to be executed, if one is present. Otherwise, it is
set to the file name used to invoke bash, as given by argument
zero.
_ At shell startup, set to the absolute pathname used to invoke
the shell or shell script being executed as passed in the envi‐
ronment or argument list. Subsequently, expands to the last
argument to the previous command, after expansion. Also set to
the full pathname used to invoke each command executed and
placed in the environment exported to that command. When check‐
ing mail, this parameter holds the name of the mail file cur‐
rently being checked.
$_ may not work in every POSIX compatible sh, although it probably works in must.
$PS1 will only be set if the shell is interactive. So this should work:
if [ -z "$PS1" ]; then
echo noninteractive
else
echo interactive
fi
try tty
if tty 2>&1 |grep not ; then echo "Not a tty"; else echo "a tty"; fi
man tty :
The tty utility writes the name of the terminal attached to standard
input to standard output. The name that is written is the string
returned by ttyname(3). If the standard input is not a terminal, the
message ``not a tty'' is written.
You could try using something like...
if [[ -t 0 ]]
then
echo "Interactive...say something!"
read line
echo $line
else
echo "Not Interactive"
fi
The "-t" switch in the test field checks if the file descriptor given matches a terminal (you could also do this to stop the program if the output was going to be printed to a terminal, for example). Here it checks if the standard in of the program matches a terminal.
Simple answer: don't run those commands inside ` ` or [ ].
There is no need for either of those constructs here.
Obviously I can't be sure what you expected
[ `echo \$- | grep -qs i` ]
to be testing, but I don't think it's testing what you think it's testing.
That code will do the following:
Run echo \$- | grep -qs i inside a subshell (due to the ` `).
Capture the subshell's standard output.
Replace the original ` ` expression with a string containing that output.
Pass that string as an argument to the [ command or built-in (depending on your shell).
Produce a successful return code from [ only if that string was nonempty (assuming the string didn't look like an option to [).
Some possible problems:
The -qs options to grep should cause it to produce no output, so I'd expect [ to be testing an empty string regardless of what $- looks like.
It's also possible that the backslash is escaping the dollar sign and causing a literal 'dollar minus' (rather than the contents of a variable) to be sent to grep.
On the other hand, if you removed the [ and backticks and instead said
if echo "$-" | grep -qs i ; then
then:
your current shell would expand "$-" with the value you want to test,
echo ... | would send that to grep on its standard input,
grep would return a successful return code when that input contained the letter i,
grep would print no output, due to the -qs flags, and
the if statement would use grep's return code to decide which branch to take.
Also:
no backticks would replace any commands with the output produced when they were run, and
no [ command would try to replace the return code of grep with some return code that it had tried to reconstruct by itself from the output produced by grep.
For more on how to use the if command, see this section of the excellent BashGuide.
If you want to test the value of $- without forking an external process (e.g. grep) then you can use the following technique:
if [ "${-%i*}" != "$-" ]
then
echo Interactive shell
else
echo Not an interactive shell
fi
This deletes any match for i* from the value of $- then checks to see if this made any difference.
(The ${parameter/from/to} construct (e.g. [ "${-//[!i]/}" = "i" ] is true iff interactive) can be used in Bash scripts but is not present in Dash, which is /bin/sh on Debian and Ubuntu systems.)

Resources