0

If I run this on the command line, it will make the commit be at the specified date:

THE_TIME='2022-01-01T22:50:12 -0700' GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -am 'commit'

However, the moment I put that into a file.sh script (chmod 0755), and run it like ./file.sh, it doesn't seem to pick up those variables. Any ideas how to get the git commit command to pick up these temporary environment variables in a bash script?

I have tried 2 things, both which didn't work. Instead of using the specified date, it uses the current date.

First, I tried just doing it like the above but in the script:

update() {
  local name="$1"
  echo "$name"
  cd "./$name"
  git add .
  THE_TIME='2022-01-01T22:50:12 -0700' GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -am 'commit'
  git push
  cd ..
}

update my-repo update my-second-repo

... apply to many repos

Second, I tried putting them into a separate file and loading them with source ./vars.sh, like this:

# vars.sh
THE_TIME='2022-01-01T22:50:12 -0700'
GIT_AUTHOR_DATE=$THE_TIME
GIT_COMMITTER_DATE=$THE_TIME
# script.sh
source ./vars.sh

update() { local name="$1" echo "$name" cd "./$name" git add . git commit -am 'commit' git push cd .. }

update my-repo update my-second-repo

... apply to many repos

What am I doing wrong? Why isn't git picking up the environment variables? I am on the latest 13.0.1 Mac OS.

2 Answers2

2

For setting environment variables, as distinct from local script variables, use the export command like this:

export GIT_AUTHOR_DATE=$THE_TIME

For more information see :

harrymc
  • 498,455
2

Background

There are few things to explain.

Each process has its own environment. The environment contains zero or more environment variables. Each variable is a value (a string) associated to a name (also a string). An instance of a shell (like Bash) is a process, so there is some environment associated to it.

In a shell you can define, access and modify shell variables. Like environment variables, these are strings with names. A given variable is not necessarily in the environment of the shell. When a shell starts, environment variables appear in the shell as shell variables, but then you can create variables that are not in the environment. A variable in the environment of the shell is called "exported variable".

The POSIX shell (sh), all (almost-)POSIX-compliant shells (e.g. Bash) and some deliberately-deviating-from-POSIX shells (e.g. Zsh) use the same interface to access environment variables and other shell variables. If you want to get the value of a variable named foo from the environment or of a shell variable named foo that is not in the environment, you use $foo. Note this was a design decision, things could have been designed differently (e.g. in Python you access environment variables via os.environ which is different than accessing "normal" variables like x).

If you want to run something with name=value in its environment, there are several methods:

  1. If name is already exported, you set it to the desired value and then execute something:

    name=value
    something
    

    The process will inherit the environment from its parent, i.e. from the shell, thus name=value will be in the environment of the process. Note name=value will remain in the environment of the shell.

  2. Regardless if name is in the environment of the shell or not, you can export it, so from now on name is in the environment for sure. Obviously you should set the variable to the desired value. All this can be done in two steps:

    name=value
    export name
    

    or in one step:

    export name=value
    

    Then you execute something. Like previously, name=value will remain in the environment of the shell.

Note: the methods below should work even if name is not in the environment of the shell, so if you want to really test them then you should un-export the variable with export -n name (the command is not portable, it works in Bash).

  1. You can tell the shell to execute something with name=value in the environment, without affecting the environment of the shell:

    name=value something
    

    (Note this is different than name=value; something. The latter is equivalent to the method number 1; it will only work if name is already exported.)

    It's possible to put more than one variable into the environment of something:

    name=value foo=bar baz=… something
    
  2. You can run another program that will inherit environment variables from the shell and run something with tailored environment. The right program is env:

    env name=value something
    

    Like the previous method, this one does not alter the environment of the shell. Here name=value is just one of the arguments for env, a string that has nothing to do with variables until env interprets it. The method may look like a useless longer alternative to the previous one, but it has its advantages:

    • env easily allows you to run something with name removed from the environment:

       env -u name something
      

      It's useful when name is in the environment of the shell and you want it to stay, still you don't want it in the environment of something. Doing this solely in a shell is possible, but somewhat cumbersome.

    • env allows you to use names that are valid for environment variables but invalid for shell variables. Example:

       env b-b=1 something
      

      As env without arguments prints its own environment, you can test the method really works by running env b-b=1 env; in the output you will see b-b=1. The previous method (b-b=1 env) does not work with this name.

Technical note: when modifying exported variables, un-exporting or exporting, the shell is not obliged to actually change its environment. It's enough if it behaves as if its environment changed. Therefore statements like "after export name=value name is in the environment" may or may not be true. "name is exported" is a better, stricter term; it means things work as if name was in the environment of the shell, even if actually it's not.


Your specific case

The command that worked for you

THE_TIME='2022-01-01T22:50:12 -0700' GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -am 'commit'

uses the name=value something method. The important fact is name=value does not affect the current shell; your THE_TIME=… did not affect the shell. In your command $THE_TIME later in the line was expanded to the value of the shell variable (exported or not) named THE_TIME. Apparently this variable existed at the time and its value was sane in the context of git, therefore the command worked and you thought it was good code. The problem is the variable certainly did not come from this very line. Your THE_TIME=… only affected what git (and possibly its children, if any) saw in its environment.

In the script the same command did not work because $THE_TIME expanded to something else; most likely to an empty string due to THE_TIME neither having been initiated from the environment nor having been declared in the script.

If you did:

THE_TIME='2022-01-01T22:50:12 -0700'; GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -am 'commit'
#                note the semicolon ^

then the shell would interpret THE_TIME=…; first and it would set its own variable (exported or not) named THE_TIME. The rest of the line would be interpreted later, so both instances of $THE_TIME would be expanded as you expect. THE_TIME would not get to the environment of git though (unless it has been exported earlier).

In your vars.sh+script.sh try the variables would get to the environment of git only if they were already exported, i.e. if you run script.sh with variables of the same names in the environment. Their existence in the environment of the shell interpreting the script would make them exported and your assignments would overwrite the values coming from the initial environment with the desired ones.

This did not happen, the relevant variables were not in the environment.

To fix the scripts you should export the relevant variables before calling git. It doesn't matter if you do it in vars.sh or in script.sh, because when you source one from the other, it's the same shell process that interprets their content.

Side note: if you source vars.sh also from another script and if you export the variables in vars.sh, then in the other script they will be exported; not exporting in vars.sh allows you to independently export (or not) the variables in script.sh and in the other script.

An alternative fix is to use the name=value something method. Because of THE_TIME existing as a shell variable, even your original line would work. Without any export, instead of git commit -am 'commit' you can do:

GIT_AUTHOR_DATE="$THE_TIME" GIT_COMMITTER_DATE="$THE_TIME" git commit -am 'commit'

And if you also want THE_TIME to get to the environment of git then you should prepend THE_TIME="$THE_TIME" to the line. Alone it looks like a no-op, like assigning the variable to itself, but in THE_TIME="$THE_TIME" something it's copying the shell variable (exported or not) to the environment of something.

exporting is easier, but if you want some variable(s) in the environment of one specific child process (among many) then the alternative fix is useful.


Notes

  • Quoting. IMO it's better always to double-quote variable substitutions, even if you know when it's safe not to quote and when it's unsafe. Scenarios where one mustn't quote are rare (and some of them appear because one doesn't know the right way; example), therefore always quoting puts you on the safe side without effort of wondering every time if in this particular case you can get away with omitting quotes or not.

    In the context of your question:

    • GIT_AUTHOR_DATE=$THE_TIME (as a complete command) is safe without double-quotes around $THE_TIME. The GIT_AUTHOR_DATE= part must not be quoted.
    • export GIT_AUTHOR_DATE=$THE_TIME is safe without double-quotes around $THE_TIME in Bash, but other shells may split+glob unquoted $THE_TIME while parsing this. You may or may not quote GIT_AUTHOR_DATE=, it's a safe string. Any valid name for a shell variable (plus =) is a safe string.
    • GIT_AUTHOR_DATE=$THE_TIME something is safe without double-quotes around $THE_TIME in Bash, but other shells may split+glob unquoted $THE_TIME while parsing this. The GIT_AUTHOR_DATE= part must not be quoted.
    • env GIT_AUTHOR_DATE=$THE_TIME something is not safe without double-quotes around $THE_TIME in many shells including Bash. You may or may not quote GIT_AUTHOR_DATE=, it's a safe string. Any valid name for a shell variable (plus =) is a safe string, but since with env you can create environment variables with other names (e.g. *), in general you may need to quote the whole name=value part.
  • Naming. See the paragraph named TEST.sh in this other answer of mine. Use the right shebang (see what happens with no shebang) and stop adding this .sh to filenames.