When reading the man pages for Git commands, you will often see an optional --
(dash dash). In my experience, the --
is not necessary and makes no difference. When do you need it? What does it mean in general, given that it appears in so many commands?
The double dash --
in git means different things to different commands, but in general it separates options from parameters.
In git specifically, the meaning of --
depends on which subcommand you are using it with. It usually separates subcommand arguments (like the branch name in git checkout
) from revisions or filenames. Sometimes it is completely optional and used only to prevent an unusual filename from being interpreted as program options.
For Example
git checkout
. To check out a "commit" (referred to as "tree-ish" in the manual, because you can actually specify a range of object types) you usegit checkout <commit>
To refine the checkout to just a file or two, use
--
to separate the "tree-ish" parameters from the "filenames" you wish to check out.git commit
. To commit whatever is in the "index" (ie, what you have staged viagit add
, simply issue thegit commit
command.git commit
[-m message]To ignore whatever you have added via
git add
and commit the changes in a specific file, usegit commit -- <filename>
git add
. To commit a file whose name begins with a-
or a--
, you must tell git add to stop reading parameters, and start reading filenames;--
does that.git add -- -sample.txt
git log
. To see the commit history restricted to only commits affecting a file usegit log -- <filename>
You need to check the man pages for any git command you use if you need to understand its specific meaning.
This question asks for a conceptual understanding of the double-dash across all git commands.
The double-dash, which signals the end of options, has been recognized as "not enough" for Git.
With Git 2.24 (Q3 2019), the command line parser learned the "--end-of-options
" notation:
The standard convention for scripters to have hardcoded set of options first on the command line, and force the command to treat end-user input as non-options, has been to use "--
" as the delimiter, but that would not work for commands that use "--
" as a delimiter between revs and pathspec.
See commit 67feca3, commit 51b4594, commit 19e8789 (06 Aug 2019) by Jeff King (peff
).
(Merged by Junio C Hamano -- gitster
-- in commit 4a12f89, 09 Sep 2019)
revision
: allow--end-of-options
to end option parsing
There's currently no robust way to tell Git that a particular option is meant to be a revision, and not an option.
So if you have a branch "refs/heads/--foo
", you cannot just say:git rev-list --foo
You can say:
git rev-list refs/heads/--foo
But that breaks down if you don't know the refname, and in particular if you're a script passing along a value from elsewhere.
In most programs, you can use "--
" to end option parsing, like this:some-prog -- "$revision"
But that doesn't work for the revision parser, because "
--
" is already meaningful there: it separates revisions from pathspecs.
So we need some other marker to separate options from revisions.This patch introduces "
--end-of-options
", which serves that purpose:git rev-list --oneline --end-of-options "$revision"
will work regardless of what's in "$revision" (well, if you say "
--
" it may fail, but it won't do something dangerous, like triggering an unexpected option).The name is verbose, but that's probably a good thing; this is meant to be used for scripted invocations where readability is more important than terseness.
One alternative would be to introduce an explicit option to mark a revision, like:
git rev-list --oneline --revision="$revision"
That's slightly more informative than this commit (because it makes even something silly like "--" unambiguous). But the pattern of using a separator like "
--
" is well established in git and in other commands, and it makes some scripting tasks simpler like:git rev-list --end-of-options "$@"
parse-options: allow
--end-of-options
as a synonym for "--"
The revision option parser recently learned about
--end-of-options
, but that's not quite enough for all callers.
Some of them, likegit-log
, pick out some options usingparse_options()
, and then feed the remainder tosetup_revisions()
.
For those cases we need to stopparse_options()
from finding more options when it sees--end-of-options
, and to retain that option inargv
so thatsetup_revisions()
can see it as well.Let's handle this the same as we do "
--
". We can even piggy-back on the handling ofPARSE_OPT_KEEP_DASHDASH
, because any caller that wants to retain one will want to retain the other.
Example:
git update-ref refs/heads/--source HEAD &&\
git log --end-of-options --source
With Git 2.30 (Q1 2021), "git rev-parse
"(man) learned the "--end-of-options
" to help scripts to safely take a parameter that is supposed to be a revision, e.g. "git rev-parse --verify -q --end-of-options $rev
(man)".
See commit 3a1f91c, commit 9033add, commit e05e2ae (10 Nov 2020) by Jeff King (peff
).
(Merged by Junio C Hamano -- gitster
-- in commit 0dd171f, 21 Nov 2020)
rev-parse
: handle--end-of-options
Signed-off-by: Jeff King
We taught rev-list a new way to separate options from revisions in 19e8789b23 ("
revision
: allow --end-of-options to end option parsing", 2019-08-06, Git v2.24.0-rc0 -- merge listed in batch #2), butrev-parse
uses its own parser.
It should know about--end-of-options
not only for consistency, but because it may be presented with similarly ambiguous cases. E.g., if a caller does:git rev-parse "$rev" -- "$path"
to parse an untrusted input, then it will get confused if
$rev
contains an option-like string like "--local-env-vars
".
Or even "--not-real
", which we'd keep as an option to pass along to rev-list.Or even more importantly:
git rev-parse --verify "$rev"
can be confused by options, even though its purpose is safely parsing untrusted input.
On the plus side, it will always fail the--verify
part, as it will not have parsed a revision, so the caller will generally "fail closed" rather than continue to use the untrusted string.
But it will still trigger whatever option was in "$rev
"; this should be mostly harmless, since rev-parse options are all read-only, but I didn't carefully audit all paths.This patch lets callers write:
git rev-parse --end-of-options "$rev" -- "$path"
and:
git rev-parse --verify --end-of-options "$rev"
which will both treat "
$rev
" always as a revision parameter.
The latter is a bit clunky. It would be nicer if we had defined "--verify
" to require that its next argument be the revision.
But we have not historically done so, and:git rev-parse --verify -q "$rev"
does currently work. I added a test here to confirm that we didn't break that.
A few implementation notes:
We don't have to re-indent the main option-parsing block, because we can combine our "did we see end of options" check with "does it start with a dash". The exception is the pre-setup options, which need their own block.
We do however have to pull the "
--
" parsing out of the "does it start with dash" block, because we want to parse it even if we've seen--end-of-options
.We'll leave "
--end-of-options
" in the output. This is probably not technically necessary, as a careful caller will do:git rev-parse --end-of-options $revs -- $paths
and anything in $revs will be resolved to an object id.
However, it does help a slightly less careful caller like:git rev-parse --end-of-options $revs_or_paths
where a path "
--foo
" will remain in the output as long as it also exists on disk.
In that case, it's helpful to retain--end-of-options
to get passed along to rev-list, as it would otherwise see just "--foo
".
git rev-parse
now includes in its man page:
Note that if you are verifying a name from an untrusted source, it is wise to use
--end-of-options
so that the name argument is not mistaken for another option.$ git rev-parse --verify --end-of-options $REV^{commit} $ git rev-parse --default master --verify --end-of-options $REV
With Git 2.31 (Q1 2021), "git mktag
"(man) validates its input using its own rules before writing a tag object---it has been updated to share the logic with git fsck
".
That means it also supports --end-of-options
.
See commit 06ce791 (06 Jan 2021), commit 2aa9425, commit 3f390a3, commit 9a1a3a4, commit acfc013, commit 1f3299f, commit acf9de4, commit 40ef015, commit dfe3948, commit 0c43911, commit 692654d, commit 30f882c, commit ca9a1ed, commit 47c95e7, commit 3b9e4dd, commit 5c2303e, commit 317c176, commit 0d35ccb, commit b5ca549, commit aba5377, commit 18430ed (05 Jan 2021), and commit 9ce0fc3, commit f59b61d (23 Dec 2020) by Ævar Arnfjörð Bjarmason (avar
).
(Merged by Junio C Hamano -- gitster
-- in commit c7d6d41, 25 Jan 2021)
mktag
: convert to parse-optionsSigned-off-by: Ævar Arnfjörð Bjarmason
Convert the "
mktag
" command to useparse-options.h
instead of its own ad-hoc argc handling.
This doesn't matter much in practice since it doesn't support any options, but removes another special-case in our codebase, and makes it easier to add options to it in the future.It does marginally improve the situation for programs that want to execute git commands in a consistent manner and e.g. always use
--end-of-options
.
E.g.
"gitaly
" does that, and has a blacklist of built-ins that don't support--end-of-options
.
This is one less special case for it and other similar programs to support.
Before Git 2.44 (Q1 2024), "git $cmd --end-of-options --rev -- --path
for some $cmd
failed to interpret --rev
as a rev, and "--path
" as a path.
This was fixed for many programs like "reset
" and "checkout
".
See commit 9385174 (06 Dec 2023) by Jeff King (peff
).
(Merged by Junio C Hamano -- gitster
-- in commit 9eec6a1, 20 Dec 2023)
parse-options
: decouple "--end-of-options
" and "--
"Signed-off-by: Jeff King
When we added generic end-of-options support in 51b4594 ("
parse-options
: allow--end-of-options
as a synonym for --", 2019-08-06, Git v2.24.0-rc0 -- merge listed in batch #2), we made them true synonyms.
They both stop option parsing, and they are both returned in the resulting argv if theKEEP_DASHDASH
flag is used.The hope was that this would work for all callers:
- most generic callers would not pass
KEEP_DASHDASH,
and so would just do the right thing (stop parsing there) without needing to know anything more.- callers with
KEEP_DASHDASH
were generally going to rely onsetup_revisions()
, which knew to handle--end-of-options
speciallyBut that turned out miss quite a few cases that pass
KEEP_DASHDASH
but do their own manual parsing.
For example, "git reset
"(man), "git checkout
"(man), and so on want passKEEP_DASHDASH
so they can support:git reset $revs -- $paths
but of course aren't going to actually do a traversal, so they don't call
setup_revisions()
.
And those cases currently get confused by--end-of-options
being left in place, like:$ git reset --end-of-options HEAD fatal: option '--end-of-options' must come before non-option arguments
We could teach each of these callers to handle the leftover option explicitly.
But let's try to be a bit more clever and see if we can solve it centrally inparse-options.c
.The bogus assumption here is that
KEEP_DASHDASH
tells us the caller wants to see--end-of-options
in the result.
But really, the callers which need to know that--end-of-options
was reached are those that may potentially parse more options from argv.
In other words, those that pass theKEEP_UNKNOWN_OPT
flag.If such a caller is aware of
--end-of-options
(e.g., because they callsetup_revisions()
with the result), then this will continue to do the right thing, treating anything after--end-of-options
as a non-option.And if the caller is not aware of
--end-of-options
, they are better off keeping it intact, because either:
- They are just passing the options along to somebody else anyway, in which case that somebody would need to know about the
--end-of-options
marker.- They are going to parse the remainder themselves, at which point choking on
--end-of-options
is much better than having it silently removed.
The point is to avoid option injection from untrusted command line arguments, and bailing is better than quietly treating the untrusted argument as an option.Note that this does shut the door for callers which want to know if we hit end-of-options, but don't otherwise need to keep unknown opts.
The obvious thing here is feeding it to the DWIMverify_filename()
machinery.
And indeed, this is a problem even for commands which do understand--end-of-options
already.
For example, without this patch, you get:$ git log --end-of-options --foo fatal: option '--foo' must come before non-option arguments
because we refuse to accept "
--foo
" as a filename (because it starts with a dash) even though we could know that we saw end-of-options.
Theverify_filename()
function simply doesn't accept this extra information.So that is the status quo, and this patch doubles down further on that.
Commands like "git reset
" have the same problem, but they won't even know that parse-options saw--end-of-options
! So even if we fixedverify_filename()
, they wouldn't have anything to pass to it.But in practice I don't think this is a big deal.
If you are being careful enough to use--end-of-options
, then you should also be using "--
" to disambiguate and avoid the DWIM behavior in the first place.
In other words, doing:git log --end-of-options --this-is-a-rev -- --this-is-a-path
works correctly, and will continue to do so.
And likewise, with this patch now:git reset --end-of-options --this-is-a-rev -- --this-is-a-path
will work, as well.
--foo
as a branch, tag or filename". But it's nice to be able to write robust scripts that could deal with such repos safely. –
Leckie © 2022 - 2024 — McMap. All rights reserved.
git checkout
. – Vasilikivasilis