CLI::getOpt()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php declare( strict_types=1 );
2
3
namespace BotRiconferme;
4
5
/**
6
 * CLI helper
7
 *
8
 * Example options:
9
 *
10
 * 'username' => 'BotRiconferme'
11
 * 'list-title' => 'Utente:BotRiconferme/List.json',
12
 * 'config-title' => 'Utente:BotRiconferme/Config.json',
13
 * 'msg-title' => 'Utente:BotRiconferme/Messages.json"
14
 *
15
 * --password=(BotPassword)
16
 * OR
17
 * --use-password-file
18
 * which will look for a PASSWORD_FILE file in the current directory containing only the plain password
19
 *
20
 * --private-password=(BotPassword)
21
 * OR
22
 * --use-private-password-file
23
 * which will look for a PRIVATE_PASSWORD_FILE file in the current directory containing only the plain password
24
 *
25
 * --tasks=update-list
26
 * OR
27
 * --subtasks=user-notice
28
 * (or comma-separated list, for both)
29
 */
30
class CLI {
31
	public const SHORT_OPTS = '';
32
33
	public const LONG_OPTS = [
34
		'username:',
35
		'list-title:',
36
		'config-title:',
37
		'msg-title:',
38
39
		'force-url:',
40
41
		'password:',
42
		'use-password-file',
43
		'private-password:',
44
		'use-private-password-file',
45
46
		'error-title:',
47
48
		'tasks:',
49
		'subtasks:'
50
	];
51
52
	public const REQUIRED_OPTS = [
53
		'username',
54
		'list-title',
55
		'config-title',
56
		'msg-title',
57
	];
58
59
	/** @todo Make it customizable? */
60
	public const PASSWORD_FILE = __DIR__ . '/../password.txt';
61
	public const PRIVATE_PASSWORD_FILE = __DIR__ . '/../private-password.txt';
62
63
	/** @var string[] */
64
	private $opts;
65
66
	/**
67
	 * @return bool
68
	 */
69
	public static function isCLI(): bool {
70
		return PHP_SAPI === 'cli';
71
	}
72
73
	/**
74
	 * Populate options and check for required ones
75
	 */
76
	public function __construct() {
77
		/** @var string[] $opts */
78
		$opts = getopt( self::SHORT_OPTS, self::LONG_OPTS );
79
		$this->checkRequiredOpts( $opts );
80
		$this->checkConflictingOpts( $opts );
81
		$this->canonicalize( $opts );
82
		$this->opts = $opts;
83
	}
84
85
	/**
86
	 * @param string[] $opts
87
	 */
88
	private function checkRequiredOpts( array $opts ): void {
89
		$missingOpts = array_diff( self::REQUIRED_OPTS, array_keys( $opts ) );
90
		if ( $missingOpts ) {
91
			$this->fatal( 'Required options missing: ' . implode( ', ', $missingOpts ) );
92
		}
93
94
		$hasPw = array_key_exists( 'password', $opts );
95
		$hasPwFile = array_key_exists( 'use-password-file', $opts );
96
		if ( !$hasPw && !$hasPwFile ) {
97
			$this->fatal( 'Please provide a password or use a password file' );
98
		}
99
100
		$hasPrivatePw = array_key_exists( 'private-password', $opts );
101
		$hasPrivatePwFile = array_key_exists( 'use-private-password-file', $opts );
102
		if ( !$hasPrivatePw && !$hasPrivatePwFile ) {
103
			$this->fatal( 'Please provide a private password or use a private-password file' );
104
		}
105
	}
106
107
	/**
108
	 * @param string[] $opts
109
	 */
110
	private function checkConflictingOpts( array $opts ): void {
111
		$this->checkNotBothSet( $opts, 'password', 'use-password-file' );
112
		if ( array_key_exists( 'use-password-file', $opts ) && !file_exists( self::PASSWORD_FILE ) ) {
113
			$this->fatal( 'Please create the password file (' . self::PASSWORD_FILE . ')' );
114
		}
115
116
		$this->checkNotBothSet( $opts, 'private-password', 'use-private-password-file' );
117
		if ( array_key_exists( 'use-private-password-file', $opts ) && !file_exists( self::PRIVATE_PASSWORD_FILE ) ) {
118
			$this->fatal( 'Please create the private-password file (' . self::PRIVATE_PASSWORD_FILE . ')' );
119
		}
120
121
		if ( count( array_intersect_key( $opts, [ 'tasks' => 1, 'subtasks' => 1 ] ) ) === 2 ) {
122
			$this->fatal( 'Cannot specify both tasks and subtasks.' );
123
		}
124
	}
125
126
	/**
127
	 * @param string[] $opts
128
	 * @param string $first
129
	 * @param string $second
130
	 */
131
	private function checkNotBothSet( array $opts, string $first, string $second ): void {
132
		if ( array_key_exists( $first, $opts ) && array_key_exists( $second, $opts ) ) {
133
			$this->fatal( "Can only use one of '$first' and '$second'" );
134
		}
135
	}
136
137
	/**
138
	 * @param string[] &$opts
139
	 */
140
	private function canonicalize( array &$opts ): void {
141
		if ( array_key_exists( 'use-password-file', $opts ) ) {
142
			$pw = trim( file_get_contents( self::PASSWORD_FILE ) );
143
			$opts['password'] = $pw;
144
			unset( $opts['use-password-file'] );
145
		}
146
		if ( array_key_exists( 'use-private-password-file', $opts ) ) {
147
			$pw = trim( file_get_contents( self::PRIVATE_PASSWORD_FILE ) );
148
			$opts['private-password'] = $pw;
149
			unset( $opts['use-private-password-file'] );
150
		}
151
	}
152
153
	/**
154
	 * @param string $msg
155
	 * @return never
156
	 */
157
	private function fatal( string $msg ): void {
158
		exit( $msg . "\n" );
159
	}
160
161
	/**
162
	 * Get an option that is known to be set.
163
	 * @param string $opt
164
	 * @return string
165
	 */
166
	public function getSetOpt( string $opt ): string {
167
		return $this->opts[$opt];
168
	}
169
170
	/**
171
	 * @param string $opt
172
	 * @param string|null $default
173
	 * @return string|null
174
	 */
175
	public function getOpt( string $opt, string $default = null ): ?string {
176
		return $this->opts[$opt] ?? $default;
177
	}
178
179
	/**
180
	 * @return string[] Either [ 'tasks' => name1,... ] or [ 'subtasks' => name1,... ]
181
	 */
182
	public function getTaskOpt(): array {
183
		return array_intersect_key(
184
			$this->opts,
185
			[ 'tasks' => true, 'subtasks' => true ]
186
		);
187
	}
188
189
	/**
190
	 * @return string|null
191
	 */
192
	public function getURL(): ?string {
193
		return $this->getOpt( 'force-url' );
194
	}
195
}
196