aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--LICENSE2
-rw-r--r--Makefile5
-rw-r--r--README203
-rw-r--r--sshcd.1361
-rw-r--r--sshexec.1249
-rw-r--r--sshexec.c683
6 files changed, 1338 insertions, 165 deletions
diff --git a/LICENSE b/LICENSE
index 0be2ccf..73634bb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
ISC License
-© 2023 Mattias Andrée <maandree@kth.se>
+© 2023, 2024, 2025 Mattias Andrée <m@maandree.se>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
diff --git a/Makefile b/Makefile
index 9dc18bf..679ce17 100644
--- a/Makefile
+++ b/Makefile
@@ -21,11 +21,16 @@ install: sshexec
mkdir -p -- "$(DESTDIR)$(PREFIX)/bin"
mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/"
cp -- sshexec "$(DESTDIR)$(PREFIX)/bin/"
+ test ! -d "$(DESTDIR)$(PREFIX)/bin/sshcd"
+ ln -sf -- sshexec "$(DESTDIR)$(PREFIX)/bin/sshcd"
cp -- sshexec.1 "$(DESTDIR)$(MANPREFIX)/man1/"
+ cp -- sshcd.1 "$(DESTDIR)$(MANPREFIX)/man1/"
uninstall:
-rm -f -- "$(DESTDIR)$(PREFIX)/bin/sshexec"
+ -rm -f -- "$(DESTDIR)$(PREFIX)/bin/sshcd"
-rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/sshexec.1"
+ -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/sshcd.1"
clean:
-rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda
diff --git a/README b/README
index 3661b2e..222445a 100644
--- a/README
+++ b/README
@@ -2,20 +2,25 @@ NAME
sshexec - run a command through ssh(1) with normal command syntax
SYNOPSIS
- sshexec [{ [ssh=ssh-command] [dir=directory] }] [ssh-option] ...
- destination command [argument] ...
+ sshexec [{ [ssh=ssh-command] [dir=directory] [cd=(strict|lax)]
+ [[fd]{>,>>,>|,<,<>}[&]=file] [asis=asis-marker
+ [nasis=asis-count]] }] [ssh-option] ... destination
+ command [argument] ...
DESCRIPTION
The sshexec utility is a wrapper for SSH that makes it easy to
run commands directly in the SSH command.
sshexec passes any argument after } to ssh-command (ssh if not
- specified), and only modifies command [argument] ... and inserts
- extra arguments after destination (it may also add a -- argument
- immediately before destination) to cause the remote shell it
- change working directory to directory, if specified, and execute
- the provided command and arguments as a regular command rather
- than as shell code joined by together by spaces.
+ specified), but it rewrites command and the arguments to one
+ argument that can be passed into ssh(1) to describe each argument
+ as separate arguments. It may also rewrite destination to remove
+ information that's not supported by ssh(1) and inserts extra
+ arguments after it (it may also add a -- argument immediately
+ before destination) to cause the remote shell it change working
+ directory to directory, if specified, and execute the provided
+ command and arguments as a regular command rather than as shell
+ code joined by together by spaces.
OPTIONS
sshexec options may be placed at the very beginning enclosed
@@ -39,14 +44,98 @@ OPTIONS
In the remote, change working directory to directory
before executing command.
+ cd=strict
+ Fail without executing the command if it's not possible
+ to set directory as the remote working directory.
+
+ cd=lax
+ Continue (but warn) executing the command even if it's
+ not possible to set directory as the remote working
+ directory.
+
+ [fd]>=file
+ After changing working directory (assuming one is
+ specified), create or truncate the specified file and
+ open it for writing, using file descriptor number fd.
+ (Default fd is 1 (standard output).)
+
+ [fd]>>=file
+ After changing working directory (assuming one is
+ specified), create the specified file if it does not
+ exist and open it for writing in append-mode, using
+ file descriptor number fd. (Default fd is 1
+ (standard output).)
+
+ [fd]>|=file
+ After changing working directory (assuming one is
+ specified), create the specified file, but fail if it
+ already exists, and open it for writing, using file
+ descriptor number fd. (Default fd is 1 (standard
+ output).)
+
+ [fd]<=file
+ After changing working directory (assuming one is
+ specified), open the specified file, for reading, using
+ file descriptor number fd. (Default fd is 0 (standard
+ input).)
+
+ [fd]<>=file
+ After changing working directory (assuming one is
+ specified), open the specified file, for reading and
+ writing, creating it if it does not already exist,
+ using file descriptor number fd. (Default fd is 0
+ (standard input).)
+
+ [fd]{>,>>,>|}&=file
+ Duplicate the file descriptor fd giving the new file
+ descriptor the number file. (Default fd is 1 (standard
+ output).)
+
+ [fd]{>,>>,>|}&=-
+ Close the file descriptor fd. (Default fd is 1
+ (standard output).)
+
+ [fd]{<,<>}&=file
+ Duplicate the file descriptor fd giving the new file
+ descriptor the number file. (Default fd is 0 (standard
+ input).)
+
+ [fd]{<,<>}&=-
+ Close the file descriptor fd. (Default fd is 0
+ (standard input).)
+
+ asis=asis-marker
+ Any argument equal to asis-marker will be skipped over
+ and instead the next argument (regardless of whether
+ it to is equal to asis-marker) will be interpreted as
+ raw shell code string that shall be inserted without
+ escaping.
+
+ masis=asis-count
+ If specified, asis-marker shall only have it's specified
+ affect up to asis-count times.
+
OPERANDS
The following operands are supported:
destination
- This operand is passed as is (without validation) to
- the ssh(1) utility. The ssh(1) utility will expect it
- the be either in the form [user@]hostname or in the
- form ssh://[user@]hostname[:port].
+ The destination to connect and log into. It shall be
+ eitherin the form [user@]hostname[:directory] or in the
+ form ssh[exec]://[user@]hostname[:port][/directory].
+
+ user shall be the name of the remote user. If not
+ specified, the name of the local user running the
+ utility will be used.
+
+ hostname shall be the address to the remote machine.
+
+ port shall be the port or service name for the port
+ to connect to on the remote machine.
+
+ directory shall be directory to change the remote
+ working directory. This is an alternative to (with the
+ exact same behaviour) to the dir option and cannot be
+ combined with it.
command [argument] ...
Whereas the ssh(1) utility would simply join the command
@@ -56,5 +145,93 @@ OPERANDS
of the as separate arguments and cause the shell to
executing them as a non-builtin command.
+ command must not contain an equals sign (=) or be just
+ a dash ("-").
+
+ENVIRONMENT VARIABLES
+ The following environment variables affects the execution of
+ sshexec:
+
+ PATH
+ Default. See to the Base Definitions volume of POSIX.1-2017,
+ Section 8.3, Other Environment Variables. This environment
+ variable affects where the sshexec utility can find the
+ ssh(1) utility or ssh-command.
+
+ SSHEXEC_SSH
+ If set and non-empty, it overrides the default value of
+ ssh-command from ssh to the value of the variable.
+
+ SSHEXEC_OPTS_NO_ARG
+ List of options that sshexec shall interpret as ssh(1)
+ options that do not have any argument. (Default is
+ 46AaCfGgKkMNnqsTtVvXxYy, meaning the options -4, -6, -A,
+ -a, -C, -f, -G, -g, -K, -k, -M, -N, -n, -q, -s, -T, -t,
+ -V, -v, -X, -x, -Y, and -y.)
+
+ SSHEXEC_OPTS_ARG
+ List of options that sshexec shall interpret as ssh(1)
+ options that have an argument. (Default is
+ BbcDEeFIiJLlmOoPpQRSWw, meaning the options -B, -b, -c,
+ -D, -E, -e, -F, -I, -i, -J, -L, -l, -m, -O, -o, -P, -p,
+ -Q, -R, -S, -W, and -w.)
+
+ SSHEXEC_OPTS_OPT_ATTACHED_ARG
+ List of options that sshexec shall interpret as ssh(1)
+ options that have an argument only if there are
+ additional characters after the option character in the
+ same command line argument. (Default is the empty
+ string, meaning no options.)
+
+ SSHEXEC_OPTS_OPT_ARG
+ List of options that sshexec shall interpret as ssh(1)
+ options that have an argument if there are additional
+ characters after the option character in the same
+ command line argument or if argument is followed
+ directly by another argument which does not start with
+ a dash (-). (Default is the empty string, meaning no
+ options.)
+
+ SSHEXEC_LONG_OPTS_NO_ARG
+ Space-separated list of long options that sshexec shall
+ interpret as ssh(1) options that do not have any
+ argument unless it is followed directly by an equals
+ sign (=) in the same command line argument. Options
+ that do not start with two dashes (--) are silently
+ ignored. (Default is the empty string, meaning no
+ options.)
+
+ SSHEXEC_LONG_OPTS_ARG
+ Space-separated list of long options that sshexec shall
+ interpret as ssh(1) options that have an argument that
+ must either be specified in the next command line
+ argument or after an equals sign (=) the shall directly
+ follow the option string in the same command line
+ argument. Options that do not start with two dashes
+ (--) are silently ignored. (Default is the empty string,
+ meaning no options.)
+
+ SSHEXEC_LONG_OPTS_OPT_ARG
+ Space-separated list of long options that sshexec shall
+ interpret as ssh(1) options that have an argument if
+ it is the option string is is directly followed by
+ equals sign (=) in the same command line argument or if
+ argument is followed directly by another argument which
+ does not start with a dash (-). Options that do not
+ start with two dashes (--) are silently ignored.
+ (Default is the empty string, meaning no options.)
+
+ Other environment variables may affect the execution of the
+ ssh(1) utility.
+
+BUGS
+ The remote shell must be sufficiently similar to sh(1posix).
+ Namely, it must support the cd builtin command and the commands
+ exec and printf is expected by POSIX. Additionally, it must
+ support "$( )", ' ', and &&, and argument separation with the
+ SP character. The remote shell must also not treat any
+ alphanumeric character, underscore (_) or slash (/) as special
+ characters.
+
SEE ALSO
- ssh(1)
+ ssh(1), sshcd(1)
diff --git a/sshcd.1 b/sshcd.1
new file mode 100644
index 0000000..d2a0265
--- /dev/null
+++ b/sshcd.1
@@ -0,0 +1,361 @@
+.TH SSHCD 1 sshexec
+
+.SH NAME
+sshcd - open ssh(1) with a specific remote working directory
+
+.SH SYNOPSIS
+.B sshcd
+.RB [ {
+.RI [\fBssh=\fP ssh-command ]
+.RB [ cd= ( strict | lax )]
+.BR } ]
+.RI [ ssh-option ]\ ...\,
+.I destination
+
+.SH DESCRIPTION
+The
+.B sshcd
+utility is a wrapper for SSH that makes it lets the user
+specify the directory remote working directory.
+.PP
+.B sshcd
+passes any argument after
+.B }
+to
+.I ssh-command
+.RB ( ssh
+if not specified), except it may rewrite
+.I destination
+to remove information that's not supported by
+.BR ssh (1).
+
+.SH OPTIONS
+.B sshcd
+options may be placed at the very beginning enclosed with
+the arguments
+.B {
+and
+.BR } .
+.B sshcd
+options, if any, shall be placed in the same
+.B {
+.BR } -group.
+Any other option will be passed as is to the
+.BR ssh (1)
+utility or
+.IR ssh-command .
+The
+.B sshcd
+utility has a build it list of options recognised by the
+.BR ssh (1)
+utility and will not allow anything matching this list.
+The
+.B sshcd
+utility does not allow mixing options and operands: no
+option may be placed after
+.IR destination .
+.PP
+The following
+.B sshcd
+options are supported:
+.TP
+.BI ssh= ssh-command
+Instead of looking for
+.B ssh
+in
+.IR PATH ,
+the
+.B sshcd
+utility shall use
+.IR ssh-command ,
+which it will look for in
+.I PATH
+if it is only a file name (does not contain a slash
+.RB ( / )).
+.TP
+.B cd=strict
+Fail without executing the
+.I command
+if it's not possible to set
+.I directory
+as the remote working directory.
+.TP
+.B cd=lax
+Continue (but warn) executing the
+.I command
+even if it's not possible to set
+.I directory
+as the remote working directory.
+
+.SH OPERANDS
+The following operands are supported:
+.TP
+.I destination
+The destination to connect and log into. It shall be either in
+the form
+.RI [ user\fP\fB@ ] hostname [\fB:\fP directory ]
+or in the form
+.BR ssh [ cd ] :// [\fIuser @ ]\fIhostname\fP[ : \fIport\fP][ / \fIdirectory\fP].
+
+.I user
+shall be the name of the remote user. If not specified,
+the name of the local user running the utility will be used.
+
+.I hostname
+shall be the address to the remote machine.
+
+.I port
+shall be the port or service name for the port to
+connect to on the remote machine.
+
+.I directory
+shall be directory to change the remote working directory.
+
+.SH STDIN
+The
+.B sshcd
+utility itself does not use the standard input.
+
+.SH INPUT FILES
+None.
+
+.SH ENVIRONMENT VARIABLES
+The following environment variables affects the execution of
+.BR sshcd :
+.TP
+.I PATH
+Default. See to the Base Definitions volume of POSIX.1-2017, Section 8.3, Other Environment Variables.
+This environment variable affects where the
+.B sshcd
+utility can find the
+.BR ssh (1)
+utility or
+.IR ssh-command .
+.TP
+.I SSHCD_PTY_ALLOC_FLAG
+Specifies the option to pass to
+.BR ssh (1)
+to tell SSH to allocate a pseudo terminal. If unset
+.B -t
+will be used. If set but empty, no flag will be passed to
+.BR ssh (1).
+.TP
+.I SSHEXEC_SSH
+If set and non-empty, it overrides the default value of
+.I ssh-command
+from
+.B ssh
+to the value of the variable.
+.TP
+.I SSHEXEC_OPTS_NO_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that do not have any argument.
+(Default is
+.BR 46AaCfGgKkMNnqsTtVvXxYy ,
+meaning the options
+.BR -4 ,
+.BR -6 ,
+.BR -A ,
+.BR -a ,
+.BR -C ,
+.BR -f ,
+.BR -G ,
+.BR -g ,
+.BR -K ,
+.BR -k ,
+.BR -M ,
+.BR -N ,
+.BR -n ,
+.BR -q ,
+.BR -s ,
+.BR -T ,
+.BR -t ,
+.BR -V ,
+.BR -v ,
+.BR -X ,
+.BR -x ,
+.BR -Y ,
+and
+.BR -y .)
+.TP
+.I SSHEXEC_OPTS_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument.
+(Default is
+.BR BbcDEeFIiJLlmOoPpQRSWw ,
+meaning the options
+.BR -B ,
+.BR -b ,
+.BR -c ,
+.BR -D ,
+.BR -E ,
+.BR -e ,
+.BR -F ,
+.BR -I ,
+.BR -i ,
+.BR -J ,
+.BR -L ,
+.BR -l ,
+.BR -m ,
+.BR -O ,
+.BR -o ,
+.BR -P ,
+.BR -p ,
+.BR -Q ,
+.BR -R ,
+.BR -S ,
+.BR -W ,
+and
+.BR -w .)
+.TP
+.I SSHEXEC_OPTS_OPT_ATTACHED_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument only if
+there are additional characters after
+the option character in the same
+command line argument. (Default is
+the empty string, meaning no options.)
+.TP
+.I SSHEXEC_OPTS_OPT_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument if there
+are additional characters after
+the option character in the same
+command line argument or if argument is
+followed directly by another argument
+which does not start with a dash
+.RB ( - ).
+(Default is
+the empty string, meaning no options.)
+.TP
+.I SSHEXEC_LONG_OPTS_NO_ARG
+Space-separated list of long options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that do not have any argument
+unless it is followed directly by an
+equals sign
+.RB ( = )
+in the same command line argument.
+Options that do not start with two dashes
+.RB ( -- )
+are silently ignored. (Default is the
+empty string, meaning no options.)
+.TP
+.I SSHEXEC_LONG_OPTS_ARG
+Space-separated list of long options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument that must
+either be specified in the next command
+line argument or after an
+equals sign
+.RB ( = )
+the shall directly follow the option
+string in the same command line argument.
+Options that do not start with two dashes
+.RB ( -- )
+are silently ignored. (Default is the
+empty string, meaning no options.)
+.TP
+.I SSHEXEC_LONG_OPTS_OPT_ARG
+Space-separated list of long options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument if it is
+the option string is is directly followed
+by equals sign
+.RB ( = )
+in the same command line argument or if
+argument is followed directly by another
+argument which does not start with a dash
+.RB ( - ).
+Options that do not start with two dashes
+.RB ( -- )
+are silently ignored. (Default is the
+empty string, meaning no options.)
+.PP
+Other environment variables may affect the execution of the
+.BR ssh (1)
+utility.
+
+.SH ASYNCHRONOUS EVENTS
+Default.
+
+.SH STDOUT
+The
+.B sshcd
+utility itself does not use the standard output.
+
+.SH STDERR
+The standard error is used for diagnostic messages in the
+.B sshcd
+utility itself.
+
+.SH OUTPUT FILES
+None.
+
+.SH EXTENDED DESCRIPTION
+None.
+
+.SH EXIT STATUS
+The
+.B sshcd
+utility exits with the exit status of the
+.BR ssh (1)
+utility or with 255 if an error occurred.
+
+.SH CONSEQUENCES OF ERRORS
+Default.
+
+.SH APPLICATION USAGE
+None.
+
+.SH EXAMPLES
+None.
+
+.SH RATIONALE
+For historical reasons, the
+.B sshcd
+utility does not let the user add a command to run
+inside the directory. This also avoids problems; if the
+user want to run a command in a specific directory,
+.BR sshexec (1)
+lets the user do so in an intuitive manner; for other
+traditional syntax,
+.BR ssh (1)
+can still be used — specifying a
+.BR cd (1)
+command at the beginning shouldn't be a problem.
+
+.SH NOTES
+None.
+
+.SH BUGS
+None.
+
+.SH FUTURE DIRECTIONS
+None.
+
+.SH SEE ALSO
+.BR ssh (1),
+.BR sshexec (1)
+
+.SH AUTHORS
+Mattias Andrée
+.RI < m@maandree.se >
diff --git a/sshexec.1 b/sshexec.1
index 0dfe121..dc42392 100644
--- a/sshexec.1
+++ b/sshexec.1
@@ -8,9 +8,12 @@ sshexec - run a command through ssh(1) with normal command syntax
.RB [ {
.RI [\fBssh=\fP ssh-command ]
.RI [\fBdir=\fP directory ]
+.RB [ cd= ( strict | lax )]
.RB [[\fIfd\fP]{ > , >> , >| , < , <> }[ & ] = \fIfile\fP]
+.RB [ asis= \fIasis-marker\fP
+.RB [ nasis= \fIasis-count\fP]]
.BR } ]
-[ssh-option] ...\,
+.RI [ ssh-option ]\ ...\,
.I destination
.I command
.RI [ argument ]\ ...\,
@@ -20,18 +23,26 @@ The
.B sshexec
utility is a wrapper for SSH that makes it easy to run commands
directly in the SSH command.
-
+.PP
.B sshexec
passes any argument after
.B }
to
.I ssh-command
.RB ( ssh
-if not specified), and only modifies
+if not specified), but it
+rewrites
.I command
-.RB [ argument ]\ ...\,
-and inserts extra arguments after
+and the
+.IR argument s
+to one argument that can be passed into
+.BR ssh (1)
+to describe each argument as separate arguments.
+It may also rewrite
.I destination
+to remove information that's not supported by
+.BR ssh (1)
+and inserts extra arguments after it
(it may also add a
.B --
argument immediately before
@@ -99,6 +110,20 @@ In the remote, change working directory to
before executing
.IR command .
.TP
+.B cd=strict
+Fail without executing the
+.I command
+if it's not possible to set
+.I directory
+as the remote working directory.
+.TP
+.B cd=lax
+Continue (but warn) executing the
+.I command
+even if it's not possible to set
+.I directory
+as the remote working directory.
+.TP
.IB \fR[\fPfd\fP]\fP >= file
After changing working directory (assuming one is specified),
create or truncate the specified
@@ -183,19 +208,52 @@ Close the file descriptor
(Default
.I fd
is 0 (standard input).)
+.TP
+.BI asis= asis-marker
+Any
+.I argument
+equal to
+.I asis-marker
+will be skipped over and instead the next argument
+(regardless of whether it to is equal to
+.IR asis-marker )
+will be interpreted as raw shell code string that
+shall be inserted without escaping.
+.TP
+.BI masis= asis-count
+If specified,
+.I asis-marker
+shall only have it's specified affect up to
+.I asis-count
+times.
.SH OPERANDS
The following operands are supported:
.TP
.I destination
-This operand is passed as is (without validation) to the
-.BR ssh (1)
-utility. The
-.BR ssh (1)
-utility will expect it the be either in the form
-.RI [ user\fP\fB@ ] hostname
+The destination to connect and log into. It shall be either in
+the form
+.RI [ user\fP\fB@ ] hostname [\fB:\fP directory ]
or in the form
-.BR ssh:// [\fIuser @ ]\fIhostname\fP[ : \fIport\fP].
+.BR ssh [ exec ] :// [\fIuser @ ]\fIhostname\fP[ : \fIport\fP][ / \fIdirectory\fP].
+
+.I user
+shall be the name of the remote user. If not specified,
+the name of the local user running the utility will be used.
+
+.I hostname
+shall be the address to the remote machine.
+
+.I port
+shall be the port or service name for the port to
+connect to on the remote machine.
+
+.I directory
+shall be directory to change the remote working directory.
+This is an alternative to (with the exact same behaviour)
+to the
+.B dir
+option and cannot be combined with it.
.TP
.IR command \ [ argument ]\ ...\,
Whereas the
@@ -211,6 +269,12 @@ utility forces the remote shell to treat each of the
as separate arguments and cause the shell to executing
them as a non-builtin command.
+.I command
+must not contain an equals sign
+.RB ( = )
+or be just a dash
+.RB (\(dq - \(dq).
+
.SH STDIN
The
.B sshexec
@@ -223,7 +287,7 @@ None.
The following environment variables affects the execution of
.BR sshexec :
.TP
-.SH PATH
+.I PATH
Default. See to the Base Definitions volume of POSIX.1-2017, Section 8.3, Other Environment Variables.
This environment variable affects where the
.B sshexec
@@ -231,6 +295,156 @@ utility can find the
.BR ssh (1)
utility or
.IR ssh-command .
+.TP
+.I SSHEXEC_SSH
+If set and non-empty, it overrides the default value of
+.I ssh-command
+from
+.B ssh
+to the value of the variable.
+.TP
+.I SSHEXEC_OPTS_NO_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that do not have any argument.
+(Default is
+.BR 46AaCfGgKkMNnqsTtVvXxYy ,
+meaning the options
+.BR -4 ,
+.BR -6 ,
+.BR -A ,
+.BR -a ,
+.BR -C ,
+.BR -f ,
+.BR -G ,
+.BR -g ,
+.BR -K ,
+.BR -k ,
+.BR -M ,
+.BR -N ,
+.BR -n ,
+.BR -q ,
+.BR -s ,
+.BR -T ,
+.BR -t ,
+.BR -V ,
+.BR -v ,
+.BR -X ,
+.BR -x ,
+.BR -Y ,
+and
+.BR -y .)
+.TP
+.I SSHEXEC_OPTS_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument.
+(Default is
+.BR BbcDEeFIiJLlmOoPpQRSWw ,
+meaning the options
+.BR -B ,
+.BR -b ,
+.BR -c ,
+.BR -D ,
+.BR -E ,
+.BR -e ,
+.BR -F ,
+.BR -I ,
+.BR -i ,
+.BR -J ,
+.BR -L ,
+.BR -l ,
+.BR -m ,
+.BR -O ,
+.BR -o ,
+.BR -P ,
+.BR -p ,
+.BR -Q ,
+.BR -R ,
+.BR -S ,
+.BR -W ,
+and
+.BR -w .)
+.TP
+.I SSHEXEC_OPTS_OPT_ATTACHED_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument only if
+there are additional characters after
+the option character in the same
+command line argument. (Default is
+the empty string, meaning no options.)
+.TP
+.I SSHEXEC_OPTS_OPT_ARG
+List of options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument if there
+are additional characters after
+the option character in the same
+command line argument or if argument is
+followed directly by another argument
+which does not start with a dash
+.RB ( - ).
+(Default is
+the empty string, meaning no options.)
+.TP
+.I SSHEXEC_LONG_OPTS_NO_ARG
+Space-separated list of long options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that do not have any argument
+unless it is followed directly by an
+equals sign
+.RB ( = )
+in the same command line argument.
+Options that do not start with two dashes
+.RB ( -- )
+are silently ignored. (Default is the
+empty string, meaning no options.)
+.TP
+.I SSHEXEC_LONG_OPTS_ARG
+Space-separated list of long options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument that must
+either be specified in the next command
+line argument or after an
+equals sign
+.RB ( = )
+the shall directly follow the option
+string in the same command line argument.
+Options that do not start with two dashes
+.RB ( -- )
+are silently ignored. (Default is the
+empty string, meaning no options.)
+.TP
+.I SSHEXEC_LONG_OPTS_OPT_ARG
+Space-separated list of long options that
+.B sshexec
+shall interpret as
+.BR ssh (1)
+options that have an argument if it is
+the option string is is directly followed
+by equals sign
+.RB ( = )
+in the same command line argument or if
+argument is followed directly by another
+argument which does not start with a dash
+.RB ( - ).
+Options that do not start with two dashes
+.RB ( -- )
+are silently ignored. (Default is the
+empty string, meaning no options.)
.PP
Other environment variables may affect the execution of the
.BR ssh (1)
@@ -272,7 +486,9 @@ None.
None.
.SH RATIONALE
-None.
+The restrictions on
+.I command
+is in place to avoid unspecified behaviour.
.SH NOTES
None.
@@ -303,8 +519,9 @@ as special characters.
None.
.SH SEE ALSO
-.BR ssh (1)
+.BR ssh (1),
+.BR sshcd (1)
.SH AUTHORS
Mattias Andrée
-.RI < maandree@kth.se >
+.RI < m@maandree.se >
diff --git a/sshexec.c b/sshexec.c
index 1ad2cf8..7f49530 100644
--- a/sshexec.c
+++ b/sshexec.c
@@ -9,111 +9,235 @@
#include <unistd.h>
+/**
+ * The name of the process
+ */
static const char *argv0 = "sshexec";
+/**
+ * Whether the process is sshcd(1),
+ * otherwise it's sshexec(1)
+ */
+static int is_sshcd = 0;
+
+/**
+ * Print an error message and exit
+ *
+ * @param ... Message format string and arguments
+ */
#define exitf(...) (fprintf(stderr, __VA_ARGS__), exit(255))
+/**
+ * Print usage synopsis and exit
+ */
static void
usage(void)
{
- exitf("usage: %s [{ %s }] [ssh-option] ... destination command [argument] ...\n",
- argv0, "[ssh=command] [dir=directory] [[fd]{>,>>,>|,<>}[&]=file]");
+ exitf("usage: %s [{ [ssh=command]%s [cd=(strict|lax)]%s }] [ssh-option] ... destination%s\n",
+ argv0,
+ is_sshcd ? "" : " [dir=directory]",
+ is_sshcd ? "" : " [[fd]{>,>>,>|,<>}[&]=file] [asis=asis-marker [nasis=asis-count]]",
+ is_sshcd ? "" : " command [argument] ...");
}
+/**
+ * File descriptor redirection
+ */
struct redirection {
+ /**
+ * First part of the redirection string, shall not be escaped
+ */
const char *asis;
+
+ /**
+ * Second part of the redirection string, shall be escaped;
+ * or `NULL` if the entire string is contained in `.asis`
+ */
const char *escape;
};
-static const enum optclass {
- NO_RECOGNISED = 0,
- NO_ARGUMENT,
- MANDATORY_ARGUMENT
-} sshopts[(size_t)1 << CHAR_BIT] = {
-#define X(F) [F] = NO_ARGUMENT
- X('4'), X('6'), X('A'), X('a'), X('C'), X('f'), X('G'),
- X('g'), X('K'), X('k'), X('M'), X('N'), X('n'), X('q'),
- X('s'), X('T'), X('t'), X('V'), X('v'), X('X'), X('x'),
- X('Y'), X('y'),
-#undef X
-#define X(F) [F] = MANDATORY_ARGUMENT
- X('B'), X('b'), X('c'), X('D'), X('E'), X('e'), X('F'),
- X('I'), X('i'), X('J'), X('L'), X('l'), X('m'), X('O'),
- X('o'), X('P'), X('p'), X('Q'), X('R'), X('S'), X('W'),
- X('w')
-#undef X
-};
-
+/**
+ * Command being constructor for ssh(1)
+ */
static char *command = NULL;
+
+/**
+ * The number of bytes allocated to `command`
+ */
static size_t command_size = 0;
+
+/**
+ * The number of bytes written to `command`
+ */
static size_t command_len = 0;
-#define build_command(DATA, N)\
+/**
+ * The directory to use as the remote working directory
+ */
+static const char *dir = NULL;
+
+/**
+ * The ssh command to use
+ */
+static const char *ssh = NULL;
+
+/**
+ * Whether the command shall fail on failure to
+ * set remote working directory
+ */
+static int strict_cd;
+
+/**
+ * The string used to mark the next argument
+ * for unescaped inclusion in the command string
+ */
+static const char *asis = NULL;
+
+/**
+ * The number of times `asis` may be recognised
+ */
+static unsigned long long int nasis = ULLONG_MAX;
+
+/**
+ * File redirections to apply after directory switch
+ */
+static struct redirection *redirections = NULL;
+
+/**
+ * The number of elements in `redirections`
+ */
+static size_t nredirections = 0;
+
+/**
+ * The destination command line operand
+ */
+static char *destination;
+
+
+/**
+ * Add text to `command`
+ *
+ * @param TEXT:const char * The text to add to `command`
+ * @param N:size_t The number of bytes to write
+ */
+#define build_command(TEXT, N)\
do {\
build_command_reserve(N);\
- memcpy(&command[command_len], (DATA), (N));\
+ memcpy(&command[command_len], (TEXT), (N));\
command_len += (N);\
} while (0)
+/**
+ * Add text to `command`
+ *
+ * @param TEXT:const char * The text to add to `command`
+ */
#define build_command_asis(S)\
build_command(S, strlen(S))
+/**
+ * NUL-byte terminate `command`
+ */
#define finalise_command()\
build_command("", 1)
+/**
+ * Allocate space for `command`
+ *
+ * @param n The number of additional bytes (from what has currently
+ * been written, rather than currently allocated) `command`
+ * should have room for
+ */
static void
build_command_reserve(size_t n)
{
- if (n > command_size - command_len) {
- if (n < 512)
- n = 512;
- if (command_len + n > SIZE_MAX)
- exitf("%s: could not allocate enough memory\n", argv0);
- command_size = command_len + n;
- command = realloc(command, command_size);
- if (!command)
- exitf("%s: could not allocate enough memory\n", argv0);
- }
+ if (n <= command_size - command_len)
+ return;
+
+ if (n < 512)
+ n = 512;
+ if (command_len + n > SIZE_MAX)
+ exitf("%s: could not allocate enough memory\n", argv0);
+ command_size = command_len + n;
+ command = realloc(command, command_size);
+ if (!command)
+ exitf("%s: could not allocate enough memory\n", argv0);
}
+/**
+ * Escape a string and add it to `command`
+ *
+ * @param arg The string to escape and add
+ */
static void
build_command_escape(const char *arg)
{
+#define IS_ALWAYS_SAFE(C) (isalnum((C)) || (C) == '_' || (C) == '/')
+#define IS_INITIAL_SAFE(C) (isalpha((C)) || (C) == '_' || (C) == '/')
+
size_t n = 0;
+ size_t lfs = 0;
+
+ /* Quote empty string */
if (!*arg) {
build_command_asis("''");
return;
}
- while (isalnum(arg[n]) || arg[n] == '_' || arg[n] == '/')
+
+ /* If the string only contains safe characters, add it would escaping */
+ while (IS_ALWAYS_SAFE(arg[n]))
n += 1;
if (!arg[n]) {
build_command(arg, n);
return;
}
+
+ /* Escape string, using quoted printf(1) statement */
build_command_asis("\"$(printf '");
- goto start;
+ goto start; /* already have a count of safe initial characters, let's add them immidately */
while (*arg) {
- build_command_reserve(4);
- command[command_len++] = '\\';
- command[command_len] = (((unsigned char)*arg >> 6) & 7) + '0';
- command_len += (command[command_len] != '0');
- command[command_len] = (((unsigned char)*arg >> 3) & 7) + '0';
- command_len += (command[command_len] != '0');
- command[command_len++] = ((unsigned char)*arg & 7) + '0';
- arg = &arg[1];
+ /* Since process substation removes at least one terminal
+ * LF, we must hold of on adding them */
+ if (*arg == '\n') {
+ lfs += 1;
+ arg = &arg[1];
+ continue;
+ }
+
+ /* Adding any held back LF */
+ if (lfs) {
+ build_command_reserve(lfs * 2U);
+ for (; lfs; lfs--) {
+ command[command_len++] = '\\';
+ command[command_len++] = 'n';
+ }
+ }
+ /* Character is unsafe, escape it */
+ if (!IS_ALWAYS_SAFE(*arg)) {
+ build_command_reserve(4);
+ command[command_len++] = '\\';
+ command[command_len] = (((unsigned char)*arg >> 6) & 7) + '0';
+ command_len += (command[command_len] != '0');
+ command[command_len] = (((unsigned char)*arg >> 3) & 7) + '0';
+ command_len += (command[command_len] != '0');
+ command[command_len++] = ((unsigned char)*arg & 7) + '0';
+ arg = &arg[1];
+ }
+
+ /* Add any safe characters as is */
n = 0;
- while (isalpha(arg[n]) || arg[n] == '_' || arg[n] == '/')
+ while (IS_INITIAL_SAFE(arg[n]) || arg[n] == '_' || arg[n] == '/')
n += 1;
if (n) {
- while (isalnum(arg[n]) || arg[n] == '_' || arg[n] == '/')
+ while (IS_ALWAYS_SAFE(arg[n]))
n += 1;
start:
build_command(arg, n);
@@ -121,29 +245,173 @@ build_command_escape(const char *arg)
}
}
build_command_asis("\\n')\"");
+ /* Add any held back terminal LF's */
+ if (lfs) {
+ build_command_reserve(lfs + 2U);
+ command[command_len++] = '\'';
+ memset(&command[command_len], '\n', lfs);
+ command_len += lfs;
+ command[command_len++] = '\'';
+ }
+
+#undef IS_ALWAYS_SAFE
+#undef IS_INITIAL_SAFE
}
-int
-main(int argc_unused, char *argv[])
+/**
+ * Construct `command`
+ *
+ * @param argv The command to execute remotely and its arguments (`NULL` terminated)
+ */
+static void
+construct_command(char *argv[])
{
- const char *dir = NULL;
- const char *ssh = NULL;
- struct redirection *redirections = NULL;
- size_t nredirections = 0;
- const char *destination;
- char **opts, *p;
- size_t nopts;
- enum optclass class;
- const char *arg;
- char opt;
- const char **args;
+ int next_asis = 0;
size_t i;
- (void) argc_unused;
+ /* Change directory */
+ if (dir) {
+ build_command_asis("cd -- ");
+ build_command_escape(dir);
+ build_command_asis(strict_cd ? " && " : " ; ");
+ }
+
+ /* Execute command */
+ if (is_sshcd) {
+ build_command_asis("exec \"$SHELL\" -l");
+ goto command_constructed;
+ }
+ if (asis)
+ build_command_asis("( env --");
+ else
+ build_command_asis("exec env --");
+ for (; *argv; argv++) {
+ if (asis && nasis && !strcmp(*argv, asis)) {
+ nasis -= 1U;
+ next_asis = 1;
+ } else {
+ build_command_asis(" ");
+ if (next_asis) {
+ next_asis = 0;
+ build_command_asis(*argv);
+ } else {
+ build_command_escape(*argv);
+ }
+ }
+ }
+ if (asis)
+ build_command_asis(" )");
+
+ /* Set redirections */
+ for (i = 0; i < nredirections; i++) {
+ build_command_asis(" ");
+ build_command_asis(redirections[i].asis);
+ if (redirections[i].escape) {
+ build_command_asis(" ");
+ build_command_escape(redirections[i].escape);
+ }
+ }
+
+ /* NUL-terminate `command` */
+command_constructed:
+ finalise_command();
+}
+
+
+/**
+ * Replace "sshexec://" prefix in `destination`
+ * with "ssh://" and remove the directory
+ * component and, if it present, store the
+ * directory to `directory`
+ */
+static void
+extract_directory_from_destination(void)
+{
+ char *dest_dir_delim;
+ if (!is_sshcd && !strncmp(destination, "sshexec://", sizeof("sshexec://") - 1U)) {
+ memmove(&destination[sizeof("ssh") - 1U],
+ &destination[sizeof("sshexec") - 1U],
+ strlen(&destination[sizeof("sshexec") - 1U]) + 1U);
+ goto using_ssh_prefix;
+ } else if (is_sshcd && !strncmp(destination, "sshcd://", sizeof("sshcd://") - 1U)) {
+ memmove(&destination[sizeof("ssh") - 1U],
+ &destination[sizeof("sshcd") - 1U],
+ strlen(&destination[sizeof("sshcd") - 1U]) + 1U);
+ goto using_ssh_prefix;
+ } else if (!strncmp(destination, "ssh://", sizeof("ssh://") - 1U)) {
+ using_ssh_prefix:
+ dest_dir_delim = strchr(&destination[sizeof("ssh://")], '/');
+ } else {
+ dest_dir_delim = strchr(destination, ':');
+ }
+ if (dest_dir_delim) {
+ if (dir)
+ exitf("%s: directory specified both in 'dir' option and in destination operand\n", argv0);
+ *dest_dir_delim++ = '\0';
+ dir = dest_dir_delim;
+ }
+}
- if (*argv)
- argv0 = *argv++;
+
+/**
+ * Get an environment variable or a default value
+ *
+ * @param var The name of the environment variable
+ * @param def The default value to return if the environment variable is not set
+ * @return The value of the environment variable, or the default value if unset
+ */
+static const char *
+get(const char *var, const char *def)
+{
+ const char *ret = getenv(var);
+ return ret ? ret : def;
+}
+
+
+/**
+ * Check if a string appears as a word in another string
+ *
+ * @param set The string to search in; words are separated by spaces
+ * @param sought The string to search for
+ * @return 1 if the string is found, 0 otherwise
+ */
+#if defined(__GNUC__)
+__attribute__((__pure__))
+#endif
+static int
+contains(const char *set, const char *sought)
+{
+ size_t m, n = strlen(sought);
+ const char *p, *q;
+ for (p = set; (q = strchr(p, ' ')); p = &q[1]) {
+ m = (size_t)(q - p);
+ if (m == n && !strncmp(p, sought, n))
+ return 1;
+ }
+ return !strcmp(p, sought);
+}
+
+
+/**
+ * Read and parse the sshexec options
+ *
+ * @param argv The arguments from the command line after
+ * the zeroth argument
+ * @return `argv` offset to skip pass any sshexec options
+ */
+static char **
+parse_sshexec_options(char *argv[])
+{
+ const char *cd = NULL;
+ const char *nasis_str = NULL;
+ char *p;
+
+ /* Check that we have any arguments to parse, and that we
+ * have the "{" mark the beginning of sshexec options. */
+ if (!*argv || strcmp(*argv, "{"))
+ goto end_of_options;
+ argv++;
#define STORE_OPT(VARP, OPT)\
if (!strncmp(*argv, OPT"=", sizeof(OPT))) {\
@@ -152,121 +420,266 @@ main(int argc_unused, char *argv[])
continue;\
}
- if (*argv && !strcmp(*argv, "{")) {
- argv++;
- for (; *argv && strcmp(*argv, "}"); argv++) {
- STORE_OPT(&ssh, "ssh")
- STORE_OPT(&dir, "dir")
+ /* Get options */
+ for (; *argv && strcmp(*argv, "}"); argv++) {
+ /* Options recognised in both sshexec(1) and sshcd(1) */
+ STORE_OPT(&ssh, "ssh")
+ STORE_OPT(&cd, "cd")
+
+ /* The rest of the options only recognised in sshexec(1) */
+ if (is_sshcd)
+ usage();
+
+ /* Normal options */
+ STORE_OPT(&dir, "dir")
+ STORE_OPT(&asis, "asis")
+ STORE_OPT(&nasis_str, "nasis")
- p = *argv;
- while (isdigit(*p))
+ /* File descriptor redirections */
+ p = *argv;
+ while (isdigit(*p))
+ p++;
+ if (p[0] == '>')
+ p = &p[1 + (p[1] == '>' || p[1] == '|')];
+ else if (p[0] == '<')
+ p = &p[1 + (p[1] == '>')];
+ else
+ usage();
+ if (p[p[0] == '&'] != '=')
+ usage();
+ redirections = realloc(redirections, (nredirections + 1U) * sizeof(*redirections));
+ if (!redirections)
+ exitf("%s: could not allocate enough memory\n", argv0);
+ if (*p == '&') {
+ p = &p[1];
+ memmove(p, &p[1], strlen(&p[1]) + 1U);
+ if (isdigit(*p)) {
p++;
- if (p[0] == '>')
- p = &p[1 + (p[1] == '>' || p[1] == '|')];
- else if (p[0] == '<')
- p = &p[1 + (p[1] == '>')];
- else
- usage();
- if (p[p[0] == '&'] != '=')
- usage();
- redirections = realloc(redirections, (nredirections + 1U) * sizeof(*redirections));
- if (!redirections)
- exitf("%s: could not allocate enough memory\n", argv0);
- if (*p == '&') {
- p = &p[1];
- memmove(p, &p[1], strlen(&p[1]) + 1U);
- if (isdigit(*p)) {
+ while (isdigit(*p))
p++;
- while (isdigit(*p))
- p++;
- } else if (*p == '-') {
- p++;
- }
- if (*p)
- usage();
- redirections[nredirections].escape = NULL;
- } else {
- *p++ = '\0';
- redirections[nredirections].escape = p;
+ } else if (*p == '-') {
+ p++;
}
- redirections[nredirections++].asis = *argv;
+ if (*p)
+ usage();
+ redirections[nredirections].escape = NULL;
+ } else {
+ *p++ = '\0';
+ redirections[nredirections].escape = p;
}
- if (!*argv)
- usage();
- argv++;
+ redirections[nredirections++].asis = *argv;
}
+ /* Check that we have the "}" argument that marks the end of sshexec options */
+ if (!*argv)
+ usage();
+ argv++;
+
#undef STORE_OPT
- if (!ssh)
- ssh = "ssh";
+end_of_options:
+ /* Parse options and set defaults */
+
+ if (!ssh) {
+ ssh = get("SSHEXEC_SSH", "");
+ if (!*ssh)
+ ssh = "ssh";
+ }
+
+ if (!cd)
+ strict_cd = !is_sshcd;
+ else if (!strcmp(cd, "strict"))
+ strict_cd = 1;
+ else if (!strcmp(cd, "lax"))
+ strict_cd = 0;
+ else
+ usage();
+
+ if (nasis_str) {
+ char *end;
+ if (!asis || !isdigit(*nasis_str))
+ usage();
+ errno = 0;
+ nasis = strtoull(nasis_str, &end, 10);
+ if ((!nasis && errno) || *end)
+ usage();
+ }
+
+ return argv;
+}
+
+
+/**
+ * Read and parse the ssh(1) options
+ *
+ * @param argv The return value of `parse_sshexec_options`
+ * @param nopts_out Output parameter for the number of option
+ * arguments, this includes all arguments the
+ * return value offsets `argv` except the "--"
+ * (if there is one), so this includes both
+ * the options themselves and their arguments,
+ * and joined options countas one
+ * @return `argv` offset to skip pass any ssh(1) options,
+ * and any "--" immediately after them
+ */
+static char **
+parse_ssh_options(char *argv[], size_t *nopts_out)
+{
+ const char *unarged_opts;
+ const char *arged_opts;
+ const char *optatarged_opts;
+ const char *optarged_opts;
+ const char *unarged_longopts;
+ const char *arged_longopts;
+ const char *optarged_longopts;
+ const char *arg;
+ size_t nopts = 0;
+
+ /* Get option syntax from the environment */
+ unarged_opts = get("SSHEXEC_OPTS_NO_ARG", "46AaCfGgKkMNnqsTtVvXxYy");
+ arged_opts = get("SSHEXEC_OPTS_ARG", "BbcDEeFIiJLlmOoPpQRSWw");
+ optatarged_opts = get("SSHEXEC_OPTS_OPT_ATTACHED_ARG", "");
+ optarged_opts = get("SSHEXEC_OPTS_OPT_ARG", "");
+ unarged_longopts = get("SSHEXEC_LONG_OPTS_NO_ARG", "");
+ arged_longopts = get("SSHEXEC_LONG_OPTS_ARG", "");
+ optarged_longopts = get("SSHEXEC_LONG_OPTS_OPT_ARG", "");
- opts = argv;
- nopts = 0;
+ /* Count option arguments from the command line */
while (*argv) {
+ /* Break at the first non-option */
if (!strcmp(*argv, "--")) {
argv++;
break;
} else if ((*argv)[0] != '-' || !(*argv)[1]) {
break;
}
- arg = &(*argv++)[1];
+
+ arg = *argv++;
nopts++;
- while (*arg) {
- opt = *arg++;
- class = sshopts[(unsigned char)opt];
- if (class == MANDATORY_ARGUMENT) {
- if (*arg) {
- break;
- } else if (*argv) {
+ if (arg[1] == '-') {
+ /* Long option */
+ if (strchr(arg, '=')) {
+ /* Option has attach argument */
+ } else if (contains(unarged_longopts, arg)) {
+ /* Option cannot have an argument */
+ } else if (contains(arged_longopts, arg)) {
+ /* Option has detached argument */
+ if (!*argv)
+ exitf("%s: argument missing for option %s\n", argv0, arg);
+ argv++;
+ nopts++;
+ } else if (contains(optarged_longopts, arg)) {
+ /* Long option with either detached argument or no argument */
+ if (*argv && **argv != '-') {
+ /* The option has a detached argument */
argv++;
nopts++;
+ }
+ } else {
+ exitf("%s: unrecognised option %s\n", argv0, arg);
+ }
+ } else {
+ /* Short option */
+ char opt;
+ arg++;
+ while ((opt = *arg++)) {
+ if (strchr(unarged_opts, opt)) {
+ /* Option cannot have an argument */
+ } else if (strchr(arged_opts, opt)) {
+ /* Option must have an argument */
+ if (arg[1]) {
+ /* Argument is attached to option */
+ break;
+ } else if (argv[1]) {
+ /* Argument is detached from option */
+ argv++;
+ nopts++;
+ break;
+ } else {
+ exitf("%s: argument missing for option -%c\n", argv0, opt);
+ }
+ } else if (strchr(optatarged_opts, opt)) {
+ /* Option may have an attached argument, but it cannot have a detached argument */
break;
+ } else if (strchr(optarged_opts, opt)) {
+ /* Option may have an attached or detached argument */
+ if (*arg) {
+ /* Argument is attached to option */
+ } else {
+ /* Either there is no argument, or it is detached from the option */
+ if (*argv && **argv != '-') {
+ /* Argument exist and is detached. We assume that if the next
+ * argument in the command line is the option's argument unless
+ * it starts with '-'. */
+ argv++;
+ nopts++;
+ break;
+ }
+ }
} else {
- exitf("%s: argument missing for option -%c\n", argv0, opt);
+ exitf("%s: unrecognised option -%c\n", argv0, opt);
}
- } else if (class == NO_RECOGNISED) {
- exitf("%s: unrecognised option -%c\n", argv0, opt);
}
}
}
+ *nopts_out = nopts;
+ return argv;
+}
+
+
+int
+main(int argc_unused, char *argv[])
+{
+ char **opts, *p;
+ size_t nopts;
+ const char **args;
+ size_t i;
+
+ (void) argc_unused;
+
+ /* Identify used command name */
+ if (*argv)
+ argv0 = *argv++;
+ p = strrchr(argv0, '/');
+ is_sshcd = !strcmp(p ? &p[1] : argv0, "sshcd");
+
+ /* Parse options */
+ argv = parse_sshexec_options(argv);
+ argv = parse_ssh_options(opts = argv, &nopts);
+
+ /* Check operand count and separate out `destination` from the command and arguments */
destination = *argv++;
- if (!destination && !*argv)
+ if (!destination || (is_sshcd ? !!*argv : !*argv))
usage();
- if (dir) {
- build_command_asis("cd -- ");
- build_command_escape(dir);
- build_command_asis(" && ");
- }
- build_command_asis("exec --");
- for (; *argv; argv++) {
- build_command_asis(" ");
- build_command_escape(*argv);
- }
- for (i = 0; i < nredirections; i++) {
- build_command_asis(" ");
- build_command_asis(redirections[i].asis);
- if (redirections[i].escape) {
- build_command_asis(" ");
- build_command_escape(redirections[i].escape);
- }
- }
- finalise_command();
+ /* Validate `destination` */
+ if (!strcmp(destination, "-"))
+ exitf("%s: the command argument must not be \"-\"\n", argv0);
+ else if (strchr(destination, '='))
+ exitf("%s: the command argument must contain an \'=\'\n", argv0);
+
+ /* Parse command line operands */
+ extract_directory_from_destination();
+ construct_command(argv);
+ /* Construct arguments for ssh(1) and execute */
i = 0;
- args = calloc(5 + nopts, sizeof(*args));
+ args = calloc(5U + (size_t)is_sshcd + nopts, sizeof(*args));
if (!args)
exitf("%s: could not allocate enough memory\n", argv0);
args[i++] = ssh;
+ if (is_sshcd) {
+ const char *pty_alloc_flag = get("SSHCD_PTY_ALLOC_FLAG", "-t");
+ if (*pty_alloc_flag)
+ args[i++] = pty_alloc_flag;
+ }
memcpy(&args[i], opts, nopts * sizeof(*opts));
i += nopts;
args[i++] = "--";
args[i++] = destination;
args[i++] = command;
args[i++] = NULL;
-
#if defined(__GNUC__)
# pragma GCC diagnostic ignored "-Wcast-qual"
#endif