128

I want to assign the result of an expression (i.e., the output from a command) to a variable and then manipulate it – for example, concatenate it with a string, then echo it.  Here's what I've got:

#!/bin/bash
cd ~/Desktop;
thefile= ls -t -U | grep -m 1 "Screen Shot";
echo "Most recent screenshot is: "$thefile;

But that outputs:

Screen Shot 2011-07-03 at 1.55.43 PM.png
Most recent screenshot is: 

So, it looks like that isn't getting assigned to $thefile, and is being printed as it's executed.

What am I missing?

1

3 Answers 3

165

A shell assignment is a single word, with no space after the equal sign. So what you wrote assigns an empty value to thefile; furthermore, since the assignment is grouped with a command, it makes thefile an environment variable and the assignment is local to that particular command, i.e. only the call to ls sees the assigned value.

You want to capture the output of a command, so you need to use command substitution:

thefile=$(ls -t -U | grep -m 1 "Screen Shot")

(Some literature shows an alternate syntax thefile=`ls …` ; the backquote syntax is equivalent to the dollar-parentheses syntax except that quoting inside backquotes is weird sometimes, so just use $(…).)

Other remarks about your script:

  • Combining -t (sort by time) with -U (don't sort with GNU ls) doesn't make sense; just use -t.

  • Rather than using grep to match screenshots, it's clearer to pass a wildcard to ls and use head to capture the first file:

      thefile=$(ls -td -- *"Screen Shot"* | head -n 1)
    
  • It's generally a bad idea to parse the output of ls. This could fail quite badly if you have file names with nonprintable characters. However, sorting files by date is difficult without ls, so it's an acceptable solution if you know you won't have unprintable characters or backslashes in file names.

  • Always use double quotes around variable substitutions, i.e. here write

      echo "Most recent screenshot is: $thefile"
    

    Without double quotes, the value of the variable is reexpanded, which will cause trouble if it contains whitespace or other special characters.

  • You don't need semicolons at the end of a line. They're redundant but harmless.

  • In a shell script, it's often a good idea to include set -e. This tells the shell to exit if any command fails (by returning a nonzero status).

If you have GNU find and sort (in particular if you're running non-embedded Linux or Cygwin), there's another approach to finding the most recent file: have find list the files and their dates, and use sort and read (here assuming bash or zsh for -d '' to read a NUL-delimited record) to extract the youngest file.

IFS=/ read -rd '' ignored thefile < <(
  find -maxdepth 1 -type f -name "*Screen Shot*" -printf "%T@/%p\0" |
    sort -rnz)

If you're willing to write this script in zsh instead of bash, there's a much easier way to catch the newest file, because zsh has glob qualifiers that permit wildcard matches not only on names but also on file metadata. The (om[1]) part after the pattern is the glob qualifiers; om sorts matches by increasing age (i.e. by modification time, newest first) and [1] extracts the first match only. The whole match needs to be in parentheses because it's technically an array, since globbing returns a list of files, even if the [1] means that in this particular case the list contains (at most) one file.

#!/bin/zsh
set -e
cd ~/Desktop
thefile=(*"Screen Shot"*(om[1]))
print -r "Most recent screenshot is: $thefile"
2
  • 12
    Wow! More info than I possibly could have hoped for. Thank you many times for your answer; I really appreciate all the effort you put into this! You've just shown me that I really, really have a lot to learn. I'm off to buy a book on bash :P.
    – Nathan G.
    Commented Jul 4, 2011 at 0:10
  • 4
    That's what I would call the definitive answer! :)
    – alex
    Commented Jul 4, 2011 at 6:19
7

If you want to do it with multiline/multiple command/s then you can do this:

output=$( bash <<EOF
#multiline/multiple command/s
EOF
)

Or:

output=$(
#multiline/multiple command/s
)

Example:

#!/bin/bash
output="$( bash <<EOF
echo first
echo second
echo third
EOF
)"
echo "$output"

Output:

first
second
third
1
  • 2
    This is a nice answer. is there a way that I can specify a variable as part of the command? for example output=$(echo $someVariable)
    – Zach Smith
    Commented Apr 4, 2019 at 12:32
5

For completeness, in different shells:

Bourne shell

The Bourne shell from the late 70s was the shell that introduced command substitution. It's no longer in use nowadays but a number of shells have their syntax based on that of that shell.

var=`any shell code`

Interprets the any shell code in a child process where the output is then going to a pipe. Meanwhile, the shell reads that output from the other end of the pipe and stores it in the $var variable.

Note however that all the trailing newline characters in that output are removed.

The Bourne shell had no array variable support, but you could store the words of the output of commands into the positional parameters ($1, $2...) with:

set -- `some code`

Or:

set x `some code`; shift

In earlier versions where -- was not supported.

The output of some code, beside the trailing newline stripping was also subject to $IFS-based splitting + globbing. For instance, if some code outputs foo,/*<newline><newline> and $IFS contains ,, $1 will contain /foo and the remaining arguments are all the non-hidden files in /.

csh

Also from the late 70s, early 80s, very popular at the time, still survives as tcsh on some systems despite its many shortcomings.

set array = `some code`

stores the words resulting of the splitting of the output of some code (on space, tab or newline) in the $array list variable.

set array = "`some code`"

Same except the splitting is done on newlines only (the elements of $array will be the non-empty lines of the output).

ksh88 and POSIX like shells.

These days, sh is one or other implementation of an interpreter for the standard sh language, which itself is mostly based on a subset of ksh88's, ksh88 being the last evolution of a shell written by David Korn, derived from the Bourne shell with some fixes and enhancements. Among those, there are bash, dash, ksh, yash, mksh, zsh (though for many of those, compliance to the sh specification has to be enabled via some option, or by calling them as sh).

var=$(any shell code)

It works like the Bourne shell's var=`...` except it makes nesting them a lot easier, and doesn't mess up the interpretation of backslashes within.

Though ksh had arrays, array support was not included in the POSIX sh specification.

rc and derivatives

rc was the shell of Research Unix V10 in the late 80s and plan9, the once to be successor of Unix. There also exists a public domain clone of that shell which spawned a few derivatives: es and akanga at least. It's got a much better syntax than the ones above, but never really took off unfortunately.

array = `cmd
array = ` {any shell code}

Stores the output of any shell code, split on the characters of $ifs into the $array.

array = `` (chars) {any shell code}

Same except that the specified chars are used instead of $ifs to do the splitting. With var = ``(){shell code}, the output is stored exactly with no splitting (the only shell that can actually do that out of the box).

ksh

ksh was the first Bourne-like shell to introduce arrays in the early 80s. Syntax was:

set -A array -- $(some code)

Like in the Bourne shell, it does trailing newline stripping, $IFS-splitting and globbing.

some command | read var1 var2 var3

Can also be used to read the first line of the output of some command, split it on $IFS (but where backslash can be used to escape the separators and newline) and store the result in those variables.

It also works in zsh, but not in some other ksh-like shells such as bash, yash or pdksh/mksh where that read is run in a subshell (for sh, the standard leaves it unspecified whether than happens or not, so you can't use that portably in sh). In bash, shopt -s lastpipe helps in non-interactive invocations of the shell.

some code | read -A array

Same except the words of that first line are stored in the array elements (bash renamed the option to -a for its own read builtin).

ksh also introduced co-processes.

some command |&
IFS= read -r first_line <&p
IFS= read -r second_line <&p

Could be used for instance to read the first and second line from the output of some command into respective variables.

zsh and bash also added co-process support later with different syntax.

zsh

zsh (another Bourne-like shell but also with features from csh and rc) introduced array support in version 2.0 in 1991 (a few months after its first version was released the previous year).

array=( $(some code) )

Would do newline stripping and IFS-splitting (not globbing) and assign the resulting words to the elements of the array.

That syntax was later copied by ksh93 in 1993 and bash 2.0 in 1996, though they do globbing on top like ksh88.

To split with things other than $IFS in zsh:

array=( ${(f)"$(some code)"} ) # split on lineFeed aka newline
array=( ${(0)"$(some code)"} ) # split on NUL
array=( ${(s[string])"$(some code)"} ) # split on any string

zsh is also the only shell that can store NUL characters in its variables, so the only shell that doesn't choke on the output of commands that contain such characters. NUL is actually in the default value of $IFS in zsh (in addition of space, tab and newline like in the other Bourne-like shells).

some command | IFS= read -rd '' var

Can be used to store the output of some command up to the first NUL character into $var. Since NUL cannot occur in text, that's a way to store text as is. -d is from ksh93, but -d '' to delimit on NULs is a bash/zsh addition.

zsh also added some more options to read such as -k to read arbitrary numbers of characters (was initially meant to read key presses, but can be used for arbitrary characters when combined with -u to specify the fd to read from). ksh93 and later bash added -N/-n options with related semantics:

some command | read -u0 -k10 var

Stores the first 10 characters of the output of some command in $var.

A sysread building in the zsh/system module can also be used to read input in blocks as a raw'er interface to the read() system call.

A zpty builtin from the eponymous module can also be used to interact with a command via a pseudo-terminal pair, allowing you to send input and read its output in a more interactive fashion like expect does.

ksh93

A complete rewrite of ksh by its author released in late 1993 and which has heavily inspired bash 2+ (and to some extent zsh and mksh)

var=${
  any shell code
}

That's the same as var=$(any shell code) except any shell code doesn't run in a subshell environment, which means it's slightly more efficient as the shell doesn't need to same and restore the environment after and means, modifications that you do inside persist afterwards.

It's also supported by recent versions of mksh.

bash

readarray array < <(some command)

stores the lines of some command (including their delimiter which you can remove with the -t option) in the elements of the $array array. <(...) called process substitution was added to ksh in the mid 80s, but couldn't be used as the target of a redirection initially (was addressed in ksh93 much later).

With bash 4.4+, you can use different delimiters than newline including NUL:

readarray -t -d '' array < <(find . -print0)

can be used for instance to store the contents of the NUL-delimited records in the output of find into the $array.

You can also do:

read var1 var2 < <(some command)

Which works even when the lastpipe option is not enabled (also works in zsh).

yash

read var1 var2 <(any shell code)

Though it looks like ksh's process subsitution, the feature there is called process redirection, and does not expand to the path of some named pipe, but assigns the corresponding fd (here 0) to the reading end of a pipe whose other end is connected to the output of the command.

fish

A relative late comer with a significantly different syntax from other shells (often much better, though still of a moving target):

set array (some shell code)

Assigns the contents of each line of the output of the shell code to elements of the array.

some command | read var1 var2

Would read the first line, split on $IFS (though will change) or arbitrary delimiters with -d delim and stores in $var1, $var2. read -a array for the words of that line to be stored in an array.

some command | read -L var1 var2

For the first two lines in different variables.

some command | read -z var

For everything up to the first NUL (so everything for text output) to be stored in $var.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .